@marimo-team/islands 0.25.3-dev169 → 0.25.3-dev170
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-BbDrlHvG.js → ConnectedDataExplorerComponent-BbSXV-Xa.js} +8 -8
- package/dist/{ErrorBoundary-RaIwGWT3.js → ErrorBoundary-NE86qEWZ.js} +4 -4
- package/dist/{abnfDiagram-VCTEODGH-Dr-fo8_z.js → abnfDiagram-VCTEODGH-D7n07nXf.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-ByKIl9GG.js → architectureDiagram-5GKGNRK7-D6CjRib5.js} +1 -1
- package/dist/{chat-ui-BsV2WH-4.js → chat-ui-CC7GekpG.js} +34 -34
- package/dist/{chunk-IMKFNOWR-IKDP18MQ.js → chunk-IMKFNOWR-6dtPz7uS.js} +1 -1
- package/dist/{chunk-SHT3W25Y-LkIgegwh.js → chunk-SHT3W25Y-BwnFNKDA.js} +1 -1
- package/dist/{chunk-SVP7TREG-DReBYtnB.js → chunk-SVP7TREG-D2eV4vBJ.js} +1 -1
- package/dist/{chunk-TICWLB2K-DQ0A0-1D.js → chunk-TICWLB2K-BDs4-_-r.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-CwvXdCNE.js → classDiagram-ZZMXUADV-DCJnf_8v.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-D0XAxDsc.js → classDiagram-v2-VYDZK3BY-BOmSzPBD.js} +2 -2
- package/dist/{code-visibility-BMi0KLAQ.js → code-visibility-Bzvc6ajl.js} +23 -23
- package/dist/{common-BMkJrZsD.js → common-Ciw4zkh2.js} +3412 -3412
- package/dist/{cynefinDiagram-5FMLGOSQ-BFN68nHr.js → cynefinDiagram-5FMLGOSQ-Bln_Soky.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-CZt4rtLq.js → dagre-GXQ25YYZ-D-nf7WzW.js} +1 -1
- package/dist/{data-grid-overlay-editor-CCc1P42P.js → data-grid-overlay-editor-AQREmxI7.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-D5cOz47_.js → diagram-S7CK7UJ4-PsgSMkY2.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-Qgdppb4a.js → diagram-UQ7AKVKN-DfwfUuSw.js} +1 -1
- package/dist/{diagram-VX7I27RA-BsDCvily.js → diagram-VX7I27RA-C9_e-oMc.js} +1 -1
- package/dist/{diagram-Z3DM3KII-igTkGjKI.js → diagram-Z3DM3KII-CWLa5tuY.js} +1 -1
- package/dist/{dropdown-menu-BWnnAstC.js → dropdown-menu-B6ftyzOg.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-BHuZGjIr.js → ebnfDiagram-PWID7BFC-B1QRM58v.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-DKObXTVE.js → erDiagram-RLTQ6QDP-DJK_FqvW.js} +1 -1
- package/dist/{error-entries-BpUJSLUZ.js → error-entries-DgK3wUJR.js} +5640 -5591
- package/dist/{feedback-button-CMVkEos_.js → feedback-button-8a6SETmT.js} +24 -24
- package/dist/{flowDiagram-HODETNUW-DZI_kit0.js → flowDiagram-HODETNUW-Uxc25XSJ.js} +2 -2
- package/dist/{formats-CwJHWasD.js → formats-DQCyPd5Z.js} +1 -1
- package/dist/{glide-data-editor-BA2pcHSV.js → glide-data-editor-B70oke_v.js} +5 -5
- package/dist/{globals-B5jjJ0ms.js → globals-Cxd5hNlI.js} +2 -2
- package/dist/{html-to-image-CQr2-IDA.js → html-to-image-D4yG5Ru0.js} +25 -25
- package/dist/{infoDiagram-27XIBGKW-DdPl-7-n.js → infoDiagram-27XIBGKW-DmjxqwL0.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-DbyW6l59.js → ishikawaDiagram-5VMMS53U-NgCLHqQi.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-BMX9SyBS.js → kanban-definition-UXKFOSKX-CZaG-iam.js} +1 -1
- package/dist/main.js +187 -188
- package/dist/{mermaid-C8W8mTbA.js → mermaid-CSvw7c71.js} +34 -34
- package/dist/{mindmap-definition-YA3MSWOX-Zd_tAxWL.js → mindmap-definition-YA3MSWOX-DJYQBDH_.js} +1 -1
- package/dist/{number-overlay-editor-BIWiV-TU.js → number-overlay-editor-ChK-SKUk.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-DjYydPdo.js → pegDiagram-XKGWAZYB-CHKO_dAe.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-R5LKjO2n.js → pieDiagram-E7YTZNPT-DVevbMAo.js} +1 -1
- package/dist/{process-output-Uw-VJE5V.js → process-output-DxvBAeL3.js} +3 -3
- package/dist/{railroadDiagram-O6MQD6OU-C7uNKtfm.js → railroadDiagram-O6MQD6OU-Detj1kcB.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-CxAIzQfl.js → requirementDiagram-BXWQKSXE-BLFD0T_Z.js} +1 -1
- package/dist/{reveal-component-XiecZ3Ua.js → reveal-component-CXMXeDZD.js} +6 -6
- package/dist/{sequenceDiagram-WJ2MYXX4-B5tmlPd5.js → sequenceDiagram-WJ2MYXX4-BZBgatQV.js} +1 -1
- package/dist/{spec-45FnujvG.js → spec-Mnkn3XEr.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-C1hc5H7Y.js → stateDiagram-D77RDMKH-4BZW9-6_.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-DRWeqQTn.js → stateDiagram-v2-MP3YSRHH-C4sKC8Iq.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-NTLas--s.js → swimlanes-42K2YHIH-SL5yeGe2.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-ChPggljD.js → swimlanesDiagram-VR7AAH4N-DqPmAYUv.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-B4CZ9srJ.js → timeline-definition-24CTP7MA-BfXUMWkq.js} +1 -1
- package/dist/{toDate-rySyYxqk.js → toDate-TMubKIVL.js} +22 -22
- package/dist/{useAsyncData-B7MiltyV.js → useAsyncData-DjMGFzFy.js} +1 -1
- package/dist/{useDeepCompareMemoize-DZmHBD_1.js → useDeepCompareMemoize-BQR5TJot.js} +1 -1
- package/dist/{useLifecycle-DRbRhzxg.js → useLifecycle-C_GvKWbQ.js} +26 -23
- package/dist/{useTheme-TLkqJsvo.js → useTheme-Bns2gJZ6.js} +179 -178
- package/dist/{vega-component-DC6P127L.js → vega-component-BsVptuhz.js} +5 -5
- package/dist/{vennDiagram-4TSXK5OY-DJIvMt44.js → vennDiagram-4TSXK5OY-CH65znGa.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-45H0zg0u.js → wardleyDiagram-VM6X3IG4-z9pWKb2_.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-ZT-XzcPN.js → xychartDiagram-S5SC5T6Z-MxyaWjjq.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +0 -1
- package/src/components/editor/cell/StagedAICell.tsx +10 -2
- package/src/components/editor/cell/code/cell-editor.tsx +7 -14
- package/src/components/editor/chrome/wrapper/__tests__/document-sync-status.test.tsx +92 -0
- package/src/components/editor/chrome/wrapper/footer-items/document-sync-status.tsx +54 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
- package/src/core/ai/__tests__/staged-cells.test.ts +8 -5
- package/src/core/ai/staged-cells.ts +7 -2
- package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +3 -15
- package/src/core/ai/tools/edit-notebook-tool.ts +14 -3
- package/src/core/cells/__tests__/cells.test.ts +0 -297
- package/src/core/cells/__tests__/document.test.ts +410 -0
- package/src/core/cells/__tests__/flush-document.test.ts +40 -0
- package/src/core/cells/__tests__/notebook-outline.test.ts +1 -6
- package/src/core/cells/__tests__/with-document-save.test.ts +67 -0
- package/src/core/cells/cells.ts +27 -158
- package/src/core/cells/columns.ts +38 -0
- package/src/core/cells/document.ts +360 -0
- package/src/core/cells/types.ts +12 -0
- package/src/core/codemirror/cells/__tests__/extensions.test.ts +27 -1
- package/src/core/codemirror/cells/extensions.ts +14 -0
- package/src/core/codemirror/format.ts +6 -5
- package/src/core/codemirror/language/__tests__/utils.test.ts +187 -15
- package/src/core/codemirror/language/cell-code.ts +114 -0
- package/src/core/codemirror/language/extension.ts +47 -30
- package/src/core/codemirror/language/languages/sql/sql.ts +7 -17
- package/src/core/codemirror/language/utils.ts +9 -39
- package/src/core/codemirror/lsp/notebook-lsp.ts +6 -5
- package/src/core/codemirror/rtc/extension.ts +0 -1
- package/src/core/edit-app.tsx +4 -3
- package/src/core/export/layout.ts +2 -2
- package/src/core/islands/bridge.ts +0 -1
- package/src/core/network/requests-lazy.ts +0 -1
- package/src/core/network/requests-network.ts +0 -9
- package/src/core/network/requests-static.ts +0 -1
- package/src/core/network/requests-toasting.tsx +0 -1
- package/src/core/network/types.ts +0 -5
- package/src/core/notebook/document/__tests__/codemirror.test.ts +76 -14
- package/src/core/notebook/document/__tests__/hold.test.ts +88 -0
- package/src/core/notebook/document/__tests__/hooks.test.tsx +3 -3
- package/src/core/notebook/document/__tests__/lifecycle-scenarios.test.ts +3 -3
- package/src/core/notebook/document/__tests__/model.test.ts +71 -15
- package/src/core/notebook/document/__tests__/scenario-fixture.ts +1 -1
- package/src/core/notebook/document/codemirror.ts +27 -122
- package/src/core/notebook/document/hooks.tsx +2 -2
- package/src/core/notebook/document/index.ts +9 -1
- package/src/core/notebook/document/model.ts +74 -24
- package/src/core/notebook/document/sync.ts +53 -4
- package/src/core/run-app.tsx +44 -41
- package/src/core/saving/save-component.tsx +2 -5
- package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +63 -3
- package/src/core/session/active-notebook.ts +4 -0
- package/src/core/session/useNotebookSession.tsx +39 -19
- package/src/core/wasm/__tests__/bridge.test.ts +3 -3
- package/src/core/wasm/bridge.ts +5 -12
- package/src/core/wasm/worker/worker.ts +0 -9
- package/src/utils/__tests__/{createReducer.test.ts → createReducer.test.tsx} +28 -0
- package/src/utils/createReducer.ts +42 -21
- package/src/core/cells/__tests__/apply-transaction.test.ts +0 -991
- package/src/core/cells/__tests__/document-changes.test.ts +0 -1140
- package/src/core/cells/__tests__/document-roundtrip.test.ts +0 -392
- package/src/core/cells/document-changes.ts +0 -944
|
@@ -1,1140 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tests for toDocumentChanges (action → change mapping) as a pure function.
|
|
5
|
-
*
|
|
6
|
-
* Each test calls the reducer to produce prevState/newState, then calls
|
|
7
|
-
* toDocumentChanges directly — no middleware, no side effects. Basic round-trip
|
|
8
|
-
* correctness is covered by document-roundtrip.test.ts. This file focuses on:
|
|
9
|
-
*
|
|
10
|
-
* - anchorOf edge cases (before vs after, first cell, __end__)
|
|
11
|
-
* - Field name mapping (hide_code → hideCode)
|
|
12
|
-
* - Column structure actions emitting reorder-cells
|
|
13
|
-
* - Actions that should NOT emit changes
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { python } from "@codemirror/lang-python";
|
|
17
|
-
import { EditorState } from "@codemirror/state";
|
|
18
|
-
import { EditorView } from "@codemirror/view";
|
|
19
|
-
import {
|
|
20
|
-
afterAll,
|
|
21
|
-
afterEach,
|
|
22
|
-
beforeAll,
|
|
23
|
-
beforeEach,
|
|
24
|
-
describe,
|
|
25
|
-
expect,
|
|
26
|
-
it,
|
|
27
|
-
vi,
|
|
28
|
-
} from "vitest";
|
|
29
|
-
import { cellId } from "@/__tests__/branded";
|
|
30
|
-
import type { CellHandle } from "@/components/editor/notebook-cell";
|
|
31
|
-
import { adaptiveLanguageConfiguration } from "@/core/codemirror/language/extension";
|
|
32
|
-
import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
|
|
33
|
-
import { requestClientAtom } from "@/core/network/requests";
|
|
34
|
-
import type { EditRequests, RunRequests } from "@/core/network/types";
|
|
35
|
-
import { store } from "@/core/state/jotai";
|
|
36
|
-
import { Deferred } from "@/utils/Deferred";
|
|
37
|
-
import { MultiColumn } from "@/utils/id-tree";
|
|
38
|
-
import { exportedForTesting, type NotebookState } from "../cells";
|
|
39
|
-
import {
|
|
40
|
-
type CellAction,
|
|
41
|
-
abortDocumentResync,
|
|
42
|
-
beginDocumentResync,
|
|
43
|
-
coalesceChanges,
|
|
44
|
-
completeDocumentResync,
|
|
45
|
-
type DocumentChange,
|
|
46
|
-
flushDocumentChanges,
|
|
47
|
-
exportedForTesting as middlewareExports,
|
|
48
|
-
toDocumentChanges,
|
|
49
|
-
withDocumentSave,
|
|
50
|
-
} from "../document-changes";
|
|
51
|
-
import { CellId } from "../ids";
|
|
52
|
-
|
|
53
|
-
const { initialNotebookState, reducer } = exportedForTesting;
|
|
54
|
-
|
|
55
|
-
function createEditor(code: string) {
|
|
56
|
-
const state = EditorState.create({
|
|
57
|
-
doc: code,
|
|
58
|
-
extensions: [
|
|
59
|
-
python(),
|
|
60
|
-
adaptiveLanguageConfiguration({
|
|
61
|
-
cellId: cellId("cell1"),
|
|
62
|
-
completionConfig: {
|
|
63
|
-
activate_on_typing: true,
|
|
64
|
-
signature_hint_on_typing: false,
|
|
65
|
-
copilot: false,
|
|
66
|
-
codeium_api_key: null,
|
|
67
|
-
},
|
|
68
|
-
hotkeys: new OverridingHotkeyProvider({}),
|
|
69
|
-
placeholderType: "marimo-import",
|
|
70
|
-
lspConfig: {},
|
|
71
|
-
}),
|
|
72
|
-
],
|
|
73
|
-
});
|
|
74
|
-
return new EditorView({ state, parent: document.body });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Dispatch an action through the reducer and auto-create editor handles. */
|
|
78
|
-
function dispatch(state: NotebookState, action: CellAction): NotebookState {
|
|
79
|
-
const next = reducer(state, action);
|
|
80
|
-
for (const [cellIdString, handle] of Object.entries(next.cellHandles)) {
|
|
81
|
-
const cid = cellIdString as CellId;
|
|
82
|
-
if (!handle.current) {
|
|
83
|
-
const view = createEditor(next.cellData[cid].code);
|
|
84
|
-
const h: CellHandle = { editorView: view, editorViewOrNull: view };
|
|
85
|
-
next.cellHandles[cid] = { current: h };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return next;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Dispatch an action and return the changes it produces. */
|
|
92
|
-
function resolve(state: NotebookState, action: CellAction) {
|
|
93
|
-
const next = dispatch(state, action);
|
|
94
|
-
const changes = toDocumentChanges(state, next, action);
|
|
95
|
-
return { next, changes };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let state: NotebookState;
|
|
99
|
-
|
|
100
|
-
let i = 0;
|
|
101
|
-
const originalCreate = CellId.create.bind(CellId);
|
|
102
|
-
|
|
103
|
-
beforeAll(() => {
|
|
104
|
-
CellId.create = () => cellId(`${i++}`);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
beforeEach(() => {
|
|
108
|
-
i = 0;
|
|
109
|
-
state = initialNotebookState();
|
|
110
|
-
state.cellIds = MultiColumn.from([]);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
afterAll(() => {
|
|
114
|
-
CellId.create = originalCreate;
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
function setup(...codes: string[]) {
|
|
118
|
-
for (const code of codes) {
|
|
119
|
-
state = dispatch(state, {
|
|
120
|
-
type: "createNewCell",
|
|
121
|
-
payload: {
|
|
122
|
-
cellId: "__end__",
|
|
123
|
-
before: false,
|
|
124
|
-
code,
|
|
125
|
-
newCellId: CellId.create(),
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
describe("toDocumentChanges", () => {
|
|
132
|
-
describe("anchorOf edge cases", () => {
|
|
133
|
-
it("uses before anchor when new cell is first", () => {
|
|
134
|
-
setup("a");
|
|
135
|
-
const [a] = state.cellIds.inOrderIds;
|
|
136
|
-
|
|
137
|
-
const { changes } = resolve(state, {
|
|
138
|
-
type: "createNewCell",
|
|
139
|
-
payload: {
|
|
140
|
-
cellId: a,
|
|
141
|
-
before: true,
|
|
142
|
-
code: "first",
|
|
143
|
-
newCellId: CellId.create(),
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
148
|
-
[
|
|
149
|
-
{
|
|
150
|
-
"before": "0",
|
|
151
|
-
"cellId": "1",
|
|
152
|
-
"code": "first",
|
|
153
|
-
"config": {
|
|
154
|
-
"column": null,
|
|
155
|
-
"disabled": false,
|
|
156
|
-
"expand_output": false,
|
|
157
|
-
"hide_code": false,
|
|
158
|
-
},
|
|
159
|
-
"name": "_",
|
|
160
|
-
"type": "create-cell",
|
|
161
|
-
},
|
|
162
|
-
]
|
|
163
|
-
`);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("uses after anchor for __end__ insertion", () => {
|
|
167
|
-
setup("a");
|
|
168
|
-
const { changes } = resolve(state, {
|
|
169
|
-
type: "createNewCell",
|
|
170
|
-
payload: {
|
|
171
|
-
cellId: "__end__",
|
|
172
|
-
before: false,
|
|
173
|
-
code: "last",
|
|
174
|
-
newCellId: CellId.create(),
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
179
|
-
[
|
|
180
|
-
{
|
|
181
|
-
"after": "0",
|
|
182
|
-
"cellId": "1",
|
|
183
|
-
"code": "last",
|
|
184
|
-
"config": {
|
|
185
|
-
"column": null,
|
|
186
|
-
"disabled": false,
|
|
187
|
-
"expand_output": false,
|
|
188
|
-
"hide_code": false,
|
|
189
|
-
},
|
|
190
|
-
"name": "_",
|
|
191
|
-
"type": "create-cell",
|
|
192
|
-
},
|
|
193
|
-
]
|
|
194
|
-
`);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("uses before when sendToTop moves cell to first position", () => {
|
|
198
|
-
setup("a", "b", "c");
|
|
199
|
-
const [, b] = state.cellIds.inOrderIds;
|
|
200
|
-
|
|
201
|
-
const { changes } = resolve(state, {
|
|
202
|
-
type: "sendToTop",
|
|
203
|
-
payload: { cellId: b },
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
207
|
-
[
|
|
208
|
-
{
|
|
209
|
-
"before": "0",
|
|
210
|
-
"cellId": "1",
|
|
211
|
-
"type": "move-cell",
|
|
212
|
-
},
|
|
213
|
-
]
|
|
214
|
-
`);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe("field name mapping", () => {
|
|
219
|
-
it("maps hide_code to hideCode in set-config", () => {
|
|
220
|
-
setup("a");
|
|
221
|
-
const [a] = state.cellIds.inOrderIds;
|
|
222
|
-
|
|
223
|
-
const { changes } = resolve(state, {
|
|
224
|
-
type: "updateCellConfig",
|
|
225
|
-
payload: { cellId: a, config: { hide_code: true } },
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
229
|
-
[
|
|
230
|
-
{
|
|
231
|
-
"cellId": "0",
|
|
232
|
-
"column": null,
|
|
233
|
-
"disabled": false,
|
|
234
|
-
"expandOutput": false,
|
|
235
|
-
"hideCode": true,
|
|
236
|
-
"type": "set-config",
|
|
237
|
-
},
|
|
238
|
-
]
|
|
239
|
-
`);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("maps expand_output to expandOutput in set-config", () => {
|
|
243
|
-
setup("a");
|
|
244
|
-
const [a] = state.cellIds.inOrderIds;
|
|
245
|
-
|
|
246
|
-
const { changes } = resolve(state, {
|
|
247
|
-
type: "updateCellConfig",
|
|
248
|
-
payload: { cellId: a, config: { expand_output: true } },
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
252
|
-
[
|
|
253
|
-
{
|
|
254
|
-
"cellId": "0",
|
|
255
|
-
"column": null,
|
|
256
|
-
"disabled": false,
|
|
257
|
-
"expandOutput": true,
|
|
258
|
-
"hideCode": false,
|
|
259
|
-
"type": "set-config",
|
|
260
|
-
},
|
|
261
|
-
]
|
|
262
|
-
`);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("includes full CellConfig in create-cell", () => {
|
|
266
|
-
setup("a");
|
|
267
|
-
|
|
268
|
-
const { changes } = resolve(state, {
|
|
269
|
-
type: "createNewCell",
|
|
270
|
-
payload: {
|
|
271
|
-
cellId: "__end__",
|
|
272
|
-
before: false,
|
|
273
|
-
code: "hidden",
|
|
274
|
-
newCellId: CellId.create(),
|
|
275
|
-
config: { expand_output: true },
|
|
276
|
-
hideCode: true,
|
|
277
|
-
},
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
expect(changes[0]).toMatchObject({
|
|
281
|
-
type: "create-cell",
|
|
282
|
-
config: { hide_code: true, expand_output: true },
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
describe("column structure actions", () => {
|
|
288
|
-
it("dropOverNewColumn emits set-config + reorder-cells", () => {
|
|
289
|
-
setup("a", "b");
|
|
290
|
-
const [, b] = state.cellIds.inOrderIds;
|
|
291
|
-
|
|
292
|
-
const { changes } = resolve(state, {
|
|
293
|
-
type: "dropOverNewColumn",
|
|
294
|
-
payload: { cellId: b },
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
298
|
-
[
|
|
299
|
-
{
|
|
300
|
-
"cellId": "1",
|
|
301
|
-
"column": 1,
|
|
302
|
-
"disabled": false,
|
|
303
|
-
"expandOutput": false,
|
|
304
|
-
"hideCode": false,
|
|
305
|
-
"type": "set-config",
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
"cellIds": [
|
|
309
|
-
"0",
|
|
310
|
-
"1",
|
|
311
|
-
],
|
|
312
|
-
"type": "reorder-cells",
|
|
313
|
-
},
|
|
314
|
-
]
|
|
315
|
-
`);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it("addColumnBreakpoint emits set-config + reorder-cells", () => {
|
|
319
|
-
setup("a", "b", "c");
|
|
320
|
-
const [, b] = state.cellIds.inOrderIds;
|
|
321
|
-
|
|
322
|
-
const { changes } = resolve(state, {
|
|
323
|
-
type: "addColumnBreakpoint",
|
|
324
|
-
payload: { cellId: b },
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
328
|
-
[
|
|
329
|
-
{
|
|
330
|
-
"cellId": "1",
|
|
331
|
-
"column": 1,
|
|
332
|
-
"disabled": false,
|
|
333
|
-
"expandOutput": false,
|
|
334
|
-
"hideCode": false,
|
|
335
|
-
"type": "set-config",
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
"cellId": "2",
|
|
339
|
-
"column": 1,
|
|
340
|
-
"disabled": false,
|
|
341
|
-
"expandOutput": false,
|
|
342
|
-
"hideCode": false,
|
|
343
|
-
"type": "set-config",
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
"cellIds": [
|
|
347
|
-
"0",
|
|
348
|
-
"1",
|
|
349
|
-
"2",
|
|
350
|
-
],
|
|
351
|
-
"type": "reorder-cells",
|
|
352
|
-
},
|
|
353
|
-
]
|
|
354
|
-
`);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("mergeAllColumns emits set-config + reorder-cells", () => {
|
|
358
|
-
setup("a", "b", "c");
|
|
359
|
-
const [, b] = state.cellIds.inOrderIds;
|
|
360
|
-
state = dispatch(state, {
|
|
361
|
-
type: "addColumnBreakpoint",
|
|
362
|
-
payload: { cellId: b },
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
const { changes } = resolve(state, {
|
|
366
|
-
type: "mergeAllColumns",
|
|
367
|
-
payload: {},
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
371
|
-
[
|
|
372
|
-
{
|
|
373
|
-
"cellId": "1",
|
|
374
|
-
"column": 0,
|
|
375
|
-
"disabled": false,
|
|
376
|
-
"expandOutput": false,
|
|
377
|
-
"hideCode": false,
|
|
378
|
-
"type": "set-config",
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
"cellId": "2",
|
|
382
|
-
"column": 0,
|
|
383
|
-
"disabled": false,
|
|
384
|
-
"expandOutput": false,
|
|
385
|
-
"hideCode": false,
|
|
386
|
-
"type": "set-config",
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
"cellIds": [
|
|
390
|
-
"0",
|
|
391
|
-
"1",
|
|
392
|
-
"2",
|
|
393
|
-
],
|
|
394
|
-
"type": "reorder-cells",
|
|
395
|
-
},
|
|
396
|
-
]
|
|
397
|
-
`);
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
describe("cell lifecycle actions", () => {
|
|
402
|
-
it("addColumn emits create-cell + column layout changes", () => {
|
|
403
|
-
setup("a");
|
|
404
|
-
const [a] = state.cellIds.inOrderIds;
|
|
405
|
-
const columnId = state.cellIds.findWithId(a).id;
|
|
406
|
-
|
|
407
|
-
const { changes } = resolve(state, {
|
|
408
|
-
type: "addColumn",
|
|
409
|
-
payload: { columnId },
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
413
|
-
[
|
|
414
|
-
{
|
|
415
|
-
"after": "0",
|
|
416
|
-
"cellId": "1",
|
|
417
|
-
"code": "",
|
|
418
|
-
"config": {
|
|
419
|
-
"column": null,
|
|
420
|
-
"disabled": false,
|
|
421
|
-
"expand_output": false,
|
|
422
|
-
"hide_code": false,
|
|
423
|
-
},
|
|
424
|
-
"name": "_",
|
|
425
|
-
"type": "create-cell",
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
"cellId": "1",
|
|
429
|
-
"column": 1,
|
|
430
|
-
"disabled": false,
|
|
431
|
-
"expandOutput": false,
|
|
432
|
-
"hideCode": false,
|
|
433
|
-
"type": "set-config",
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
"cellIds": [
|
|
437
|
-
"0",
|
|
438
|
-
"1",
|
|
439
|
-
],
|
|
440
|
-
"type": "reorder-cells",
|
|
441
|
-
},
|
|
442
|
-
]
|
|
443
|
-
`);
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it("undoDeleteCell emits create-cell for restored cell", () => {
|
|
447
|
-
setup("a", "b");
|
|
448
|
-
const [, b] = state.cellIds.inOrderIds;
|
|
449
|
-
|
|
450
|
-
// Delete cell b, then undo
|
|
451
|
-
state = dispatch(state, {
|
|
452
|
-
type: "deleteCell",
|
|
453
|
-
payload: { cellId: b },
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
const { changes } = resolve(state, {
|
|
457
|
-
type: "undoDeleteCell",
|
|
458
|
-
payload: {},
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
// Restored cell should have original code
|
|
462
|
-
expect(changes[0]).toMatchObject({
|
|
463
|
-
type: "create-cell",
|
|
464
|
-
code: "b",
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it("splitCell emits set-code + create-cell", () => {
|
|
469
|
-
setup("line1\nline2");
|
|
470
|
-
const [a] = state.cellIds.inOrderIds;
|
|
471
|
-
|
|
472
|
-
// Position cursor at end of "line1" (position 5)
|
|
473
|
-
const view = state.cellHandles[a].current!.editorView!;
|
|
474
|
-
view.dispatch({ selection: { anchor: 5 } });
|
|
475
|
-
|
|
476
|
-
const { changes } = resolve(state, {
|
|
477
|
-
type: "splitCell",
|
|
478
|
-
payload: { cellId: a },
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
482
|
-
[
|
|
483
|
-
{
|
|
484
|
-
"cellId": "0",
|
|
485
|
-
"code": "line1",
|
|
486
|
-
"type": "set-code",
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
"after": "0",
|
|
490
|
-
"cellId": "1",
|
|
491
|
-
"code": "line2",
|
|
492
|
-
"config": {
|
|
493
|
-
"column": null,
|
|
494
|
-
"disabled": false,
|
|
495
|
-
"expand_output": false,
|
|
496
|
-
"hide_code": false,
|
|
497
|
-
},
|
|
498
|
-
"name": "_",
|
|
499
|
-
"type": "create-cell",
|
|
500
|
-
},
|
|
501
|
-
]
|
|
502
|
-
`);
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
it("undoSplitCell emits set-code + delete-cell", () => {
|
|
506
|
-
setup("line1\nline2");
|
|
507
|
-
const [a] = state.cellIds.inOrderIds;
|
|
508
|
-
|
|
509
|
-
// Split the cell first
|
|
510
|
-
const view = state.cellHandles[a].current!.editorView!;
|
|
511
|
-
view.dispatch({ selection: { anchor: 5 } });
|
|
512
|
-
const snapshot = view.state.doc.toString();
|
|
513
|
-
state = dispatch(state, {
|
|
514
|
-
type: "splitCell",
|
|
515
|
-
payload: { cellId: a },
|
|
516
|
-
});
|
|
517
|
-
const [, newCell] = state.cellIds.inOrderIds;
|
|
518
|
-
|
|
519
|
-
// Now undo the split
|
|
520
|
-
const { changes } = resolve(state, {
|
|
521
|
-
type: "undoSplitCell",
|
|
522
|
-
payload: { cellId: a, snapshot },
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
526
|
-
[
|
|
527
|
-
{
|
|
528
|
-
"cellId": "0",
|
|
529
|
-
"code": "line1
|
|
530
|
-
line2",
|
|
531
|
-
"type": "set-code",
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
"cellId": "${newCell}",
|
|
535
|
-
"type": "delete-cell",
|
|
536
|
-
},
|
|
537
|
-
]
|
|
538
|
-
`);
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it("moveToNextCell emits create-cell when past last cell", () => {
|
|
542
|
-
setup("a");
|
|
543
|
-
const [a] = state.cellIds.inOrderIds;
|
|
544
|
-
|
|
545
|
-
const { changes } = resolve(state, {
|
|
546
|
-
type: "moveToNextCell",
|
|
547
|
-
payload: { cellId: a, before: false },
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
551
|
-
[
|
|
552
|
-
{
|
|
553
|
-
"after": "0",
|
|
554
|
-
"cellId": "1",
|
|
555
|
-
"code": "",
|
|
556
|
-
"config": {
|
|
557
|
-
"column": null,
|
|
558
|
-
"disabled": false,
|
|
559
|
-
"expand_output": false,
|
|
560
|
-
"hide_code": false,
|
|
561
|
-
},
|
|
562
|
-
"name": "_",
|
|
563
|
-
"type": "create-cell",
|
|
564
|
-
},
|
|
565
|
-
]
|
|
566
|
-
`);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
it("moveToNextCell emits nothing when moving within bounds", () => {
|
|
570
|
-
setup("a", "b");
|
|
571
|
-
const [a] = state.cellIds.inOrderIds;
|
|
572
|
-
|
|
573
|
-
const { changes } = resolve(state, {
|
|
574
|
-
type: "moveToNextCell",
|
|
575
|
-
payload: { cellId: a, before: false },
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
expect(changes).toHaveLength(0);
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
it("addSetupCellIfDoesntExist emits create-cell when new", () => {
|
|
582
|
-
setup("a");
|
|
583
|
-
|
|
584
|
-
const { changes } = resolve(state, {
|
|
585
|
-
type: "addSetupCellIfDoesntExist",
|
|
586
|
-
payload: { code: "import marimo as mo" },
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
expect(changes).toMatchInlineSnapshot(`
|
|
590
|
-
[
|
|
591
|
-
{
|
|
592
|
-
"before": "0",
|
|
593
|
-
"cellId": "setup",
|
|
594
|
-
"code": "import marimo as mo",
|
|
595
|
-
"config": {
|
|
596
|
-
"column": null,
|
|
597
|
-
"disabled": false,
|
|
598
|
-
"expand_output": false,
|
|
599
|
-
"hide_code": false,
|
|
600
|
-
},
|
|
601
|
-
"name": "setup",
|
|
602
|
-
"type": "create-cell",
|
|
603
|
-
},
|
|
604
|
-
]
|
|
605
|
-
`);
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it("addSetupCellIfDoesntExist emits nothing when already exists", () => {
|
|
609
|
-
setup("a");
|
|
610
|
-
// Add setup cell first
|
|
611
|
-
state = dispatch(state, {
|
|
612
|
-
type: "addSetupCellIfDoesntExist",
|
|
613
|
-
payload: { code: "import marimo as mo" },
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
// Try to add again — should just focus
|
|
617
|
-
const { changes } = resolve(state, {
|
|
618
|
-
type: "addSetupCellIfDoesntExist",
|
|
619
|
-
payload: {},
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
expect(changes).toHaveLength(0);
|
|
623
|
-
});
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
describe("actions that should NOT emit changes", () => {
|
|
627
|
-
it("focusCell returns empty", () => {
|
|
628
|
-
setup("a", "b");
|
|
629
|
-
const [a] = state.cellIds.inOrderIds;
|
|
630
|
-
const { changes } = resolve(state, {
|
|
631
|
-
type: "focusCell",
|
|
632
|
-
payload: { cellId: a, where: "after" },
|
|
633
|
-
});
|
|
634
|
-
expect(changes).toHaveLength(0);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it("prepareForRun returns empty", () => {
|
|
638
|
-
setup("a");
|
|
639
|
-
const [a] = state.cellIds.inOrderIds;
|
|
640
|
-
const { changes } = resolve(state, {
|
|
641
|
-
type: "prepareForRun",
|
|
642
|
-
payload: { cellId: a },
|
|
643
|
-
});
|
|
644
|
-
expect(changes).toHaveLength(0);
|
|
645
|
-
});
|
|
646
|
-
});
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
describe("coalesceChanges", () => {
|
|
650
|
-
const X = cellId("X");
|
|
651
|
-
const Y = cellId("Y");
|
|
652
|
-
const A = cellId("A");
|
|
653
|
-
const config = { column: null, disabled: false, hide_code: false };
|
|
654
|
-
|
|
655
|
-
it("drops set-code that precedes a delete of the same cell", () => {
|
|
656
|
-
const changes: DocumentChange[] = [
|
|
657
|
-
{ type: "set-code", cellId: X, code: "x = 2" },
|
|
658
|
-
{ type: "delete-cell", cellId: X },
|
|
659
|
-
];
|
|
660
|
-
expect(coalesceChanges(changes)).toEqual([
|
|
661
|
-
{ type: "delete-cell", cellId: X },
|
|
662
|
-
]);
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
it("keeps create+delete for a created-then-deleted cell, dropping the edit", () => {
|
|
666
|
-
const changes: DocumentChange[] = [
|
|
667
|
-
{ type: "create-cell", cellId: X, code: "", name: "_", config },
|
|
668
|
-
{ type: "set-code", cellId: X, code: "x = 1" },
|
|
669
|
-
{ type: "delete-cell", cellId: X },
|
|
670
|
-
];
|
|
671
|
-
expect(coalesceChanges(changes)).toEqual([
|
|
672
|
-
{ type: "create-cell", cellId: X, code: "", name: "_", config },
|
|
673
|
-
{ type: "delete-cell", cellId: X },
|
|
674
|
-
]);
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
it("drops set-code, set-name, set-config, and move-cell for a deleted cell", () => {
|
|
678
|
-
const changes: DocumentChange[] = [
|
|
679
|
-
{ type: "create-cell", cellId: X, code: "", name: "_", config },
|
|
680
|
-
{ type: "set-code", cellId: X, code: "x = 1" },
|
|
681
|
-
{ type: "set-name", cellId: X, name: "foo" },
|
|
682
|
-
{
|
|
683
|
-
type: "set-config",
|
|
684
|
-
cellId: X,
|
|
685
|
-
column: null,
|
|
686
|
-
disabled: true,
|
|
687
|
-
hideCode: false,
|
|
688
|
-
expandOutput: false,
|
|
689
|
-
},
|
|
690
|
-
{ type: "move-cell", cellId: X, after: A },
|
|
691
|
-
{ type: "delete-cell", cellId: X },
|
|
692
|
-
];
|
|
693
|
-
expect(coalesceChanges(changes)).toEqual([
|
|
694
|
-
{ type: "create-cell", cellId: X, code: "", name: "_", config },
|
|
695
|
-
{ type: "delete-cell", cellId: X },
|
|
696
|
-
]);
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
it("leaves a plain delete of an unedited cell unchanged", () => {
|
|
700
|
-
const changes: DocumentChange[] = [{ type: "delete-cell", cellId: X }];
|
|
701
|
-
expect(coalesceChanges(changes)).toEqual([
|
|
702
|
-
{ type: "delete-cell", cellId: X },
|
|
703
|
-
]);
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
it("does not touch set-* for cells that are not deleted, preserving order", () => {
|
|
707
|
-
const changes: DocumentChange[] = [
|
|
708
|
-
{ type: "set-code", cellId: A, code: "a = 1" },
|
|
709
|
-
{ type: "set-code", cellId: Y, code: "y = 1" },
|
|
710
|
-
{ type: "delete-cell", cellId: X },
|
|
711
|
-
];
|
|
712
|
-
expect(coalesceChanges(changes)).toEqual([
|
|
713
|
-
{ type: "set-code", cellId: A, code: "a = 1" },
|
|
714
|
-
{ type: "set-code", cellId: Y, code: "y = 1" },
|
|
715
|
-
{ type: "delete-cell", cellId: X },
|
|
716
|
-
]);
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
it("collapses repeated set-code for a surviving cell to the last value", () => {
|
|
720
|
-
const changes: DocumentChange[] = [
|
|
721
|
-
{ type: "set-code", cellId: X, code: "x = 1" },
|
|
722
|
-
{ type: "set-name", cellId: X, name: "foo" },
|
|
723
|
-
{ type: "set-code", cellId: X, code: "x = 2" },
|
|
724
|
-
];
|
|
725
|
-
expect(coalesceChanges(changes)).toEqual([
|
|
726
|
-
{ type: "set-name", cellId: X, name: "foo" },
|
|
727
|
-
{ type: "set-code", cellId: X, code: "x = 2" },
|
|
728
|
-
]);
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
it("keeps both creates so a downstream anchor stays resolvable", () => {
|
|
732
|
-
const changes: DocumentChange[] = [
|
|
733
|
-
{ type: "create-cell", cellId: X, code: "", name: "_", config, after: A },
|
|
734
|
-
{ type: "create-cell", cellId: Y, code: "", name: "_", config, after: X },
|
|
735
|
-
{ type: "delete-cell", cellId: X },
|
|
736
|
-
];
|
|
737
|
-
expect(coalesceChanges(changes)).toEqual([
|
|
738
|
-
{ type: "create-cell", cellId: X, code: "", name: "_", config, after: A },
|
|
739
|
-
{ type: "create-cell", cellId: Y, code: "", name: "_", config, after: X },
|
|
740
|
-
{ type: "delete-cell", cellId: X },
|
|
741
|
-
]);
|
|
742
|
-
});
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
describe("document transaction middleware", () => {
|
|
746
|
-
let sent: DocumentChange[][];
|
|
747
|
-
|
|
748
|
-
const updateCode = (cellId: CellId, code: string) => {
|
|
749
|
-
state = dispatch(state, {
|
|
750
|
-
type: "updateCellCode",
|
|
751
|
-
payload: { cellId, code, formattingChange: false },
|
|
752
|
-
});
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
beforeEach(() => {
|
|
756
|
-
vi.useFakeTimers();
|
|
757
|
-
sent = [];
|
|
758
|
-
middlewareExports.cancelPendingChanges();
|
|
759
|
-
const captureClient: Pick<
|
|
760
|
-
EditRequests & RunRequests,
|
|
761
|
-
"sendDocumentTransaction"
|
|
762
|
-
> = {
|
|
763
|
-
sendDocumentTransaction: async ({ changes }) => {
|
|
764
|
-
sent.push(changes);
|
|
765
|
-
return null;
|
|
766
|
-
},
|
|
767
|
-
};
|
|
768
|
-
store.set(
|
|
769
|
-
requestClientAtom,
|
|
770
|
-
captureClient as unknown as EditRequests & RunRequests,
|
|
771
|
-
);
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
afterEach(() => {
|
|
775
|
-
middlewareExports.cancelPendingChanges();
|
|
776
|
-
store.set(requestClientAtom, null);
|
|
777
|
-
vi.useRealTimers();
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
it("coalesces edit + delete of the same cell within one debounce window", async () => {
|
|
781
|
-
setup("x = 1");
|
|
782
|
-
const [x] = state.cellIds.inOrderIds;
|
|
783
|
-
// Discard the create-cell from setup; it would flush as its own batch.
|
|
784
|
-
middlewareExports.cancelPendingChanges();
|
|
785
|
-
|
|
786
|
-
updateCode(x, "x = 2");
|
|
787
|
-
state = dispatch(state, {
|
|
788
|
-
type: "deleteCell",
|
|
789
|
-
payload: { cellId: x },
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
expect(sent).toEqual([]);
|
|
793
|
-
|
|
794
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
795
|
-
|
|
796
|
-
expect(sent).toHaveLength(1);
|
|
797
|
-
expect(sent[0]).toEqual([{ type: "delete-cell", cellId: x }]);
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
it("sends edit and delete as separate clean transactions across windows", async () => {
|
|
801
|
-
setup("x = 1");
|
|
802
|
-
const [x] = state.cellIds.inOrderIds;
|
|
803
|
-
middlewareExports.cancelPendingChanges();
|
|
804
|
-
|
|
805
|
-
updateCode(x, "x = 2");
|
|
806
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
807
|
-
|
|
808
|
-
state = dispatch(state, {
|
|
809
|
-
type: "deleteCell",
|
|
810
|
-
payload: { cellId: x },
|
|
811
|
-
});
|
|
812
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
813
|
-
|
|
814
|
-
expect(sent).toHaveLength(2);
|
|
815
|
-
expect(sent[0]).toEqual([{ type: "set-code", cellId: x, code: "x = 2" }]);
|
|
816
|
-
expect(sent[1]).toEqual([{ type: "delete-cell", cellId: x }]);
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
it("flushes pending changes before a dependent operation", async () => {
|
|
820
|
-
setup("x = 1");
|
|
821
|
-
const [x] = state.cellIds.inOrderIds;
|
|
822
|
-
middlewareExports.cancelPendingChanges();
|
|
823
|
-
updateCode(x, "x = 2");
|
|
824
|
-
|
|
825
|
-
await flushDocumentChanges();
|
|
826
|
-
|
|
827
|
-
expect(sent).toEqual([[{ type: "set-code", cellId: x, code: "x = 2" }]]);
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
it("keeps a failed transaction sticky before a dependent operation", async () => {
|
|
831
|
-
const failingClient: Pick<
|
|
832
|
-
EditRequests & RunRequests,
|
|
833
|
-
"sendDocumentTransaction"
|
|
834
|
-
> = {
|
|
835
|
-
sendDocumentTransaction: async ({ changes }) => {
|
|
836
|
-
sent.push(changes);
|
|
837
|
-
throw new Error("connection lost");
|
|
838
|
-
},
|
|
839
|
-
};
|
|
840
|
-
store.set(
|
|
841
|
-
requestClientAtom,
|
|
842
|
-
failingClient as unknown as EditRequests & RunRequests,
|
|
843
|
-
);
|
|
844
|
-
|
|
845
|
-
setup("x = 1");
|
|
846
|
-
const [x] = state.cellIds.inOrderIds;
|
|
847
|
-
middlewareExports.cancelPendingChanges();
|
|
848
|
-
updateCode(x, "x = 2");
|
|
849
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
850
|
-
|
|
851
|
-
updateCode(x, "x = 3");
|
|
852
|
-
await expect(flushDocumentChanges()).rejects.toThrow("connection lost");
|
|
853
|
-
|
|
854
|
-
expect(sent).toEqual([[{ type: "set-code", cellId: x, code: "x = 2" }]]);
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
it("resumes after a full document save establishes a fresh baseline", async () => {
|
|
858
|
-
let attempt = 0;
|
|
859
|
-
const recoveringClient: Pick<
|
|
860
|
-
EditRequests & RunRequests,
|
|
861
|
-
"sendDocumentTransaction"
|
|
862
|
-
> = {
|
|
863
|
-
sendDocumentTransaction: async ({ changes }) => {
|
|
864
|
-
sent.push(changes);
|
|
865
|
-
attempt += 1;
|
|
866
|
-
if (attempt === 1) {
|
|
867
|
-
throw new Error("connection lost");
|
|
868
|
-
}
|
|
869
|
-
return null;
|
|
870
|
-
},
|
|
871
|
-
};
|
|
872
|
-
store.set(
|
|
873
|
-
requestClientAtom,
|
|
874
|
-
recoveringClient as unknown as EditRequests & RunRequests,
|
|
875
|
-
);
|
|
876
|
-
|
|
877
|
-
setup("x = 1");
|
|
878
|
-
const [x] = state.cellIds.inOrderIds;
|
|
879
|
-
middlewareExports.cancelPendingChanges();
|
|
880
|
-
updateCode(x, "x = 2");
|
|
881
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
882
|
-
|
|
883
|
-
updateCode(x, "x = 3");
|
|
884
|
-
await withDocumentSave(async () => {
|
|
885
|
-
updateCode(x, "x = 4");
|
|
886
|
-
});
|
|
887
|
-
|
|
888
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
889
|
-
expect(sent).toEqual([
|
|
890
|
-
[{ type: "set-code", cellId: x, code: "x = 2" }],
|
|
891
|
-
[{ type: "set-code", cellId: x, code: "x = 4" }],
|
|
892
|
-
]);
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
it("restores unsent edits when a full document save fails", async () => {
|
|
896
|
-
const firstStarted = new Deferred<void>();
|
|
897
|
-
const firstRelease = new Deferred<void>();
|
|
898
|
-
const failingClient: Pick<
|
|
899
|
-
EditRequests & RunRequests,
|
|
900
|
-
"sendDocumentTransaction"
|
|
901
|
-
> = {
|
|
902
|
-
sendDocumentTransaction: async ({ changes }) => {
|
|
903
|
-
sent.push(changes);
|
|
904
|
-
if (sent.length === 1) {
|
|
905
|
-
firstStarted.resolve();
|
|
906
|
-
await firstRelease.promise;
|
|
907
|
-
throw new Error("connection lost");
|
|
908
|
-
}
|
|
909
|
-
return null;
|
|
910
|
-
},
|
|
911
|
-
};
|
|
912
|
-
store.set(
|
|
913
|
-
requestClientAtom,
|
|
914
|
-
failingClient as unknown as EditRequests & RunRequests,
|
|
915
|
-
);
|
|
916
|
-
|
|
917
|
-
setup("x = 1");
|
|
918
|
-
const [x] = state.cellIds.inOrderIds;
|
|
919
|
-
middlewareExports.cancelPendingChanges();
|
|
920
|
-
updateCode(x, "x = 2");
|
|
921
|
-
vi.advanceTimersByTime(400);
|
|
922
|
-
await firstStarted.promise;
|
|
923
|
-
|
|
924
|
-
updateCode(x, "x = 3");
|
|
925
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
926
|
-
firstRelease.resolve();
|
|
927
|
-
await expect(flushDocumentChanges()).rejects.toThrow("connection lost");
|
|
928
|
-
|
|
929
|
-
updateCode(x, "x = 4");
|
|
930
|
-
const firstResync = beginDocumentResync();
|
|
931
|
-
abortDocumentResync(firstResync);
|
|
932
|
-
const secondResync = beginDocumentResync();
|
|
933
|
-
expect(secondResync?.includedTransactions).toEqual([
|
|
934
|
-
[{ type: "set-code", cellId: x, code: "x = 3" }],
|
|
935
|
-
]);
|
|
936
|
-
expect(secondResync?.includedChanges).toEqual([
|
|
937
|
-
{ type: "set-code", cellId: x, code: "x = 4" },
|
|
938
|
-
]);
|
|
939
|
-
await completeDocumentResync(secondResync);
|
|
940
|
-
|
|
941
|
-
updateCode(x, "x = 5");
|
|
942
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
943
|
-
expect(sent.at(-1)).toEqual([
|
|
944
|
-
{ type: "set-code", cellId: x, code: "x = 5" },
|
|
945
|
-
]);
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
it("keeps edits debounced while an earlier transaction is in flight", async () => {
|
|
949
|
-
const firstRelease = new Deferred<void>();
|
|
950
|
-
const firstStarted = new Deferred<void>();
|
|
951
|
-
const delayedClient: Pick<
|
|
952
|
-
EditRequests & RunRequests,
|
|
953
|
-
"sendDocumentTransaction"
|
|
954
|
-
> = {
|
|
955
|
-
sendDocumentTransaction: async ({ changes }) => {
|
|
956
|
-
sent.push(changes);
|
|
957
|
-
if (sent.length === 1) {
|
|
958
|
-
firstStarted.resolve();
|
|
959
|
-
await firstRelease.promise;
|
|
960
|
-
}
|
|
961
|
-
return null;
|
|
962
|
-
},
|
|
963
|
-
};
|
|
964
|
-
store.set(
|
|
965
|
-
requestClientAtom,
|
|
966
|
-
delayedClient as unknown as EditRequests & RunRequests,
|
|
967
|
-
);
|
|
968
|
-
|
|
969
|
-
setup("x = 1");
|
|
970
|
-
const [x] = state.cellIds.inOrderIds;
|
|
971
|
-
middlewareExports.cancelPendingChanges();
|
|
972
|
-
updateCode(x, "x = 2");
|
|
973
|
-
vi.advanceTimersByTime(400);
|
|
974
|
-
await firstStarted.promise;
|
|
975
|
-
|
|
976
|
-
updateCode(x, "x = 3");
|
|
977
|
-
firstRelease.resolve();
|
|
978
|
-
await vi.advanceTimersByTimeAsync(399);
|
|
979
|
-
expect(sent).toHaveLength(1);
|
|
980
|
-
|
|
981
|
-
await vi.advanceTimersByTimeAsync(1);
|
|
982
|
-
expect(sent).toEqual([
|
|
983
|
-
[{ type: "set-code", cellId: x, code: "x = 2" }],
|
|
984
|
-
[{ type: "set-code", cellId: x, code: "x = 3" }],
|
|
985
|
-
]);
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
it("waits for every transaction queued while flushing", async () => {
|
|
989
|
-
const releases = Array.from({ length: 3 }, () => new Deferred<void>());
|
|
990
|
-
const starts = Array.from({ length: 3 }, () => new Deferred<void>());
|
|
991
|
-
const orderedClient: Pick<
|
|
992
|
-
EditRequests & RunRequests,
|
|
993
|
-
"sendDocumentTransaction"
|
|
994
|
-
> = {
|
|
995
|
-
sendDocumentTransaction: async ({ changes }) => {
|
|
996
|
-
const index = sent.length;
|
|
997
|
-
sent.push(changes);
|
|
998
|
-
starts[index].resolve();
|
|
999
|
-
await releases[index].promise;
|
|
1000
|
-
return null;
|
|
1001
|
-
},
|
|
1002
|
-
};
|
|
1003
|
-
store.set(
|
|
1004
|
-
requestClientAtom,
|
|
1005
|
-
orderedClient as unknown as EditRequests & RunRequests,
|
|
1006
|
-
);
|
|
1007
|
-
|
|
1008
|
-
setup("x = 1");
|
|
1009
|
-
const [x] = state.cellIds.inOrderIds;
|
|
1010
|
-
middlewareExports.cancelPendingChanges();
|
|
1011
|
-
updateCode(x, "x = 2");
|
|
1012
|
-
vi.advanceTimersByTime(400);
|
|
1013
|
-
await starts[0].promise;
|
|
1014
|
-
|
|
1015
|
-
const flushed = flushDocumentChanges();
|
|
1016
|
-
|
|
1017
|
-
updateCode(x, "x = 3");
|
|
1018
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
1019
|
-
expect(sent).toHaveLength(1);
|
|
1020
|
-
|
|
1021
|
-
releases[0].resolve();
|
|
1022
|
-
await starts[1].promise;
|
|
1023
|
-
|
|
1024
|
-
updateCode(x, "x = 4");
|
|
1025
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
1026
|
-
expect(sent).toHaveLength(2);
|
|
1027
|
-
|
|
1028
|
-
let flushResolved = false;
|
|
1029
|
-
void flushed.then(() => {
|
|
1030
|
-
flushResolved = true;
|
|
1031
|
-
});
|
|
1032
|
-
releases[1].resolve();
|
|
1033
|
-
await starts[2].promise;
|
|
1034
|
-
expect(flushResolved).toBe(false);
|
|
1035
|
-
|
|
1036
|
-
releases[2].resolve();
|
|
1037
|
-
await flushed;
|
|
1038
|
-
expect(sent).toEqual([
|
|
1039
|
-
[{ type: "set-code", cellId: x, code: "x = 2" }],
|
|
1040
|
-
[{ type: "set-code", cellId: x, code: "x = 3" }],
|
|
1041
|
-
[{ type: "set-code", cellId: x, code: "x = 4" }],
|
|
1042
|
-
]);
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
it("orders a full save between earlier and later transactions", async () => {
|
|
1046
|
-
const firstStarted = new Deferred<void>();
|
|
1047
|
-
const firstRelease = new Deferred<void>();
|
|
1048
|
-
const saveStarted = new Deferred<void>();
|
|
1049
|
-
const saveRelease = new Deferred<void>();
|
|
1050
|
-
const events: string[] = [];
|
|
1051
|
-
const delayedClient: Pick<
|
|
1052
|
-
EditRequests & RunRequests,
|
|
1053
|
-
"sendDocumentTransaction"
|
|
1054
|
-
> = {
|
|
1055
|
-
sendDocumentTransaction: async ({ changes }) => {
|
|
1056
|
-
const change = changes[0];
|
|
1057
|
-
events.push("code" in change ? change.code : change.type);
|
|
1058
|
-
if (events.length === 1) {
|
|
1059
|
-
firstStarted.resolve();
|
|
1060
|
-
await firstRelease.promise;
|
|
1061
|
-
}
|
|
1062
|
-
return null;
|
|
1063
|
-
},
|
|
1064
|
-
};
|
|
1065
|
-
store.set(
|
|
1066
|
-
requestClientAtom,
|
|
1067
|
-
delayedClient as unknown as EditRequests & RunRequests,
|
|
1068
|
-
);
|
|
1069
|
-
|
|
1070
|
-
setup("x = 1");
|
|
1071
|
-
const [x] = state.cellIds.inOrderIds;
|
|
1072
|
-
middlewareExports.cancelPendingChanges();
|
|
1073
|
-
updateCode(x, "x = 2");
|
|
1074
|
-
vi.advanceTimersByTime(400);
|
|
1075
|
-
await firstStarted.promise;
|
|
1076
|
-
|
|
1077
|
-
const saving = withDocumentSave(async () => {
|
|
1078
|
-
events.push("save");
|
|
1079
|
-
saveStarted.resolve();
|
|
1080
|
-
await saveRelease.promise;
|
|
1081
|
-
});
|
|
1082
|
-
expect(events).toEqual(["x = 2"]);
|
|
1083
|
-
|
|
1084
|
-
firstRelease.resolve();
|
|
1085
|
-
await saveStarted.promise;
|
|
1086
|
-
updateCode(x, "x = 3");
|
|
1087
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
1088
|
-
expect(events).toEqual(["x = 2", "save"]);
|
|
1089
|
-
|
|
1090
|
-
saveRelease.resolve();
|
|
1091
|
-
await saving;
|
|
1092
|
-
await flushDocumentChanges();
|
|
1093
|
-
expect(events).toEqual(["x = 2", "save", "x = 3"]);
|
|
1094
|
-
});
|
|
1095
|
-
|
|
1096
|
-
it("ignores a failure from a transaction reset while in flight", async () => {
|
|
1097
|
-
const firstStarted = new Deferred<void>();
|
|
1098
|
-
const firstRelease = new Deferred<void>();
|
|
1099
|
-
const firstFinished = new Deferred<void>();
|
|
1100
|
-
const delayedClient: Pick<
|
|
1101
|
-
EditRequests & RunRequests,
|
|
1102
|
-
"sendDocumentTransaction"
|
|
1103
|
-
> = {
|
|
1104
|
-
sendDocumentTransaction: async ({ changes }) => {
|
|
1105
|
-
sent.push(changes);
|
|
1106
|
-
if (sent.length === 1) {
|
|
1107
|
-
firstStarted.resolve();
|
|
1108
|
-
try {
|
|
1109
|
-
await firstRelease.promise;
|
|
1110
|
-
} finally {
|
|
1111
|
-
firstFinished.resolve();
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
return null;
|
|
1115
|
-
},
|
|
1116
|
-
};
|
|
1117
|
-
store.set(
|
|
1118
|
-
requestClientAtom,
|
|
1119
|
-
delayedClient as unknown as EditRequests & RunRequests,
|
|
1120
|
-
);
|
|
1121
|
-
|
|
1122
|
-
setup("x = 1");
|
|
1123
|
-
const [x] = state.cellIds.inOrderIds;
|
|
1124
|
-
middlewareExports.cancelPendingChanges();
|
|
1125
|
-
updateCode(x, "x = 2");
|
|
1126
|
-
vi.advanceTimersByTime(400);
|
|
1127
|
-
await firstStarted.promise;
|
|
1128
|
-
|
|
1129
|
-
middlewareExports.cancelPendingChanges();
|
|
1130
|
-
firstRelease.reject(new Error("stale connection failure"));
|
|
1131
|
-
await firstFinished.promise;
|
|
1132
|
-
await Promise.resolve();
|
|
1133
|
-
|
|
1134
|
-
updateCode(x, "x = 3");
|
|
1135
|
-
await flushDocumentChanges();
|
|
1136
|
-
expect(sent.at(-1)).toEqual([
|
|
1137
|
-
{ type: "set-code", cellId: x, code: "x = 3" },
|
|
1138
|
-
]);
|
|
1139
|
-
});
|
|
1140
|
-
});
|