@marimo-team/islands 0.21.2-dev57 → 0.21.2-dev58

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.
@@ -0,0 +1,279 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /**
4
+ * Tests for fromDocumentChanges / applyTransactionChanges in isolation.
5
+ *
6
+ * These test the change→action mapping for edge cases and error paths that
7
+ * can't be exercised via the round-trip tests (since toDocumentChanges would
8
+ * never produce malformed or conflicting changes). Basic correctness is
9
+ * covered by document-roundtrip.test.ts. This file focuses on:
10
+ *
11
+ * - Multi-change transactions (create+move, create+set-code, set-code+delete)
12
+ * - Cancelled changes (create+delete same cell)
13
+ * - Missing/nonexistent anchors and cells
14
+ * - Config propagation on create-cell (disabled, column)
15
+ */
16
+
17
+ import { python } from "@codemirror/lang-python";
18
+ import { EditorState } from "@codemirror/state";
19
+ import { EditorView } from "@codemirror/view";
20
+ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
21
+ import type { CellHandle } from "@/components/editor/notebook-cell";
22
+ import { adaptiveLanguageConfiguration } from "@/core/codemirror/language/extension";
23
+ import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
24
+ import { MultiColumn } from "@/utils/id-tree";
25
+ import { exportedForTesting, type NotebookState } from "../cells";
26
+ import {
27
+ applyTransactionChanges,
28
+ exportedForTesting as middlewareExports,
29
+ } from "../document-changes";
30
+ import { CellId } from "../ids";
31
+
32
+ const { initialNotebookState, reducer, createActions } = exportedForTesting;
33
+
34
+ function createEditor(code: string) {
35
+ const state = EditorState.create({
36
+ doc: code,
37
+ extensions: [
38
+ python(),
39
+ adaptiveLanguageConfiguration({
40
+ cellId: "cell1" as CellId,
41
+ completionConfig: {
42
+ activate_on_typing: true,
43
+ signature_hint_on_typing: false,
44
+ copilot: false,
45
+ codeium_api_key: null,
46
+ },
47
+ hotkeys: new OverridingHotkeyProvider({}),
48
+ placeholderType: "marimo-import",
49
+ lspConfig: {},
50
+ }),
51
+ ],
52
+ });
53
+ return new EditorView({ state, parent: document.body });
54
+ }
55
+
56
+ let state: NotebookState;
57
+ let actions: ReturnType<typeof createActions>;
58
+
59
+ function setup(...codes: string[]) {
60
+ state = initialNotebookState();
61
+ state.cellIds = MultiColumn.from([]);
62
+ actions = createActions((action) => {
63
+ state = reducer(state, action);
64
+ for (const [cellId, handle] of Object.entries(state.cellHandles)) {
65
+ if (!handle.current) {
66
+ const view = createEditor(state.cellData[cellId as CellId].code);
67
+ const h: CellHandle = { editorView: view, editorViewOrNull: view };
68
+ state.cellHandles[cellId as CellId] = { current: h };
69
+ }
70
+ }
71
+ });
72
+ for (const code of codes) {
73
+ actions.createNewCell({ cellId: "__end__", before: false, code });
74
+ }
75
+ }
76
+
77
+ afterEach(() => {
78
+ middlewareExports.cancelPendingChanges();
79
+ });
80
+
81
+ function apply(changes: Parameters<typeof applyTransactionChanges>[0]) {
82
+ applyTransactionChanges(changes, actions, () => state.cellIds.inOrderIds);
83
+ }
84
+
85
+ /** Snapshot of document state: ordering, code, name, config. */
86
+ function pretty(s: NotebookState): string {
87
+ const lines = s.cellIds.inOrderIds.map((id) => {
88
+ const cell = s.cellData[id];
89
+ const flags: string[] = [];
90
+ if (cell.name && cell.name !== "_") {
91
+ flags.push(`name=${cell.name}`);
92
+ }
93
+ if (cell.config.hide_code) {
94
+ flags.push("hide_code");
95
+ }
96
+ if (cell.config.disabled) {
97
+ flags.push("disabled");
98
+ }
99
+ if (cell.config.column != null) {
100
+ flags.push(`col=${cell.config.column}`);
101
+ }
102
+ const suffix = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
103
+ return `${id}: '${cell.code}'${suffix}`;
104
+ });
105
+ return `\n${lines.join("\n")}\n`;
106
+ }
107
+
108
+ let i = 0;
109
+
110
+ beforeAll(() => {
111
+ CellId.create = () => `${i++}` as CellId;
112
+ });
113
+
114
+ beforeEach(() => {
115
+ i = 0;
116
+ });
117
+
118
+ describe("applyTransactionChanges edge cases", () => {
119
+ it("create-cell applies disabled and column config", () => {
120
+ setup("a");
121
+ apply([
122
+ {
123
+ type: "create-cell",
124
+ cellId: "new-cell",
125
+ code: "configured",
126
+ name: "",
127
+ config: { hide_code: true, disabled: true, column: 1 },
128
+ },
129
+ ]);
130
+ expect(pretty(state)).toMatchInlineSnapshot(`
131
+ "
132
+ 0: 'a'
133
+ new-cell: 'configured' [hide_code, disabled, col=1]
134
+ "
135
+ `);
136
+ });
137
+
138
+ it("create-cell then move-cell in same transaction", () => {
139
+ setup("a", "b");
140
+ const [a] = state.cellIds.inOrderIds;
141
+ apply([
142
+ {
143
+ type: "create-cell",
144
+ cellId: "new-cell",
145
+ code: "new",
146
+ name: "",
147
+ config: {},
148
+ },
149
+ { type: "move-cell", cellId: "new-cell", before: a },
150
+ ]);
151
+ expect(pretty(state)).toMatchInlineSnapshot(`
152
+ "
153
+ new-cell: 'new'
154
+ 0: 'a'
155
+ 1: 'b'
156
+ "
157
+ `);
158
+ });
159
+
160
+ it("create-cell then set-code in same transaction", () => {
161
+ setup("a");
162
+ apply([
163
+ {
164
+ type: "create-cell",
165
+ cellId: "new-cell",
166
+ code: "initial",
167
+ name: "",
168
+ config: {},
169
+ },
170
+ { type: "set-code", cellId: "new-cell", code: "updated" },
171
+ ]);
172
+ expect(pretty(state)).toMatchInlineSnapshot(`
173
+ "
174
+ 0: 'a'
175
+ new-cell: 'updated'
176
+ "
177
+ `);
178
+ });
179
+
180
+ it("create-cell then delete-cell same cell cancels out", () => {
181
+ setup("a");
182
+ apply([
183
+ {
184
+ type: "create-cell",
185
+ cellId: "ephemeral",
186
+ code: "tmp",
187
+ name: "",
188
+ config: {},
189
+ },
190
+ { type: "delete-cell", cellId: "ephemeral" },
191
+ ]);
192
+ expect(pretty(state)).toMatchInlineSnapshot(`
193
+ "
194
+ 0: 'a'
195
+ "
196
+ `);
197
+ });
198
+
199
+ it("multiple changes in one transaction", () => {
200
+ setup("a", "b", "c");
201
+ const [a, b, c] = state.cellIds.inOrderIds;
202
+ apply([
203
+ { type: "set-code", cellId: a, code: "x = 1" },
204
+ { type: "set-name", cellId: b, name: "middle" },
205
+ { type: "delete-cell", cellId: c },
206
+ ]);
207
+ expect(pretty(state)).toMatchInlineSnapshot(`
208
+ "
209
+ 0: 'x = 1'
210
+ 1: 'b' [name=middle]
211
+ "
212
+ `);
213
+ });
214
+
215
+ it("move-cell with no anchor appends to end", () => {
216
+ setup("a", "b", "c");
217
+ const [a] = state.cellIds.inOrderIds;
218
+ apply([{ type: "move-cell", cellId: a }]);
219
+ expect(pretty(state)).toMatchInlineSnapshot(`
220
+ "
221
+ 1: 'b'
222
+ 2: 'c'
223
+ 0: 'a'
224
+ "
225
+ `);
226
+ });
227
+
228
+ it("move-cell with missing after anchor falls back to end", () => {
229
+ setup("a", "b");
230
+ const [a] = state.cellIds.inOrderIds;
231
+ apply([{ type: "move-cell", cellId: a, after: "nonexistent" as CellId }]);
232
+ expect(pretty(state)).toMatchInlineSnapshot(`
233
+ "
234
+ 1: 'b'
235
+ 0: 'a'
236
+ "
237
+ `);
238
+ });
239
+
240
+ it("move-cell with missing before anchor falls back to start", () => {
241
+ setup("a", "b");
242
+ const [, b] = state.cellIds.inOrderIds;
243
+ apply([{ type: "move-cell", cellId: b, before: "nonexistent" as CellId }]);
244
+ expect(pretty(state)).toMatchInlineSnapshot(`
245
+ "
246
+ 1: 'b'
247
+ 0: 'a'
248
+ "
249
+ `);
250
+ });
251
+
252
+ it("move-cell on nonexistent cell is a no-op", () => {
253
+ setup("a", "b");
254
+ apply([
255
+ {
256
+ type: "move-cell",
257
+ cellId: "nonexistent" as CellId,
258
+ after: "0" as CellId,
259
+ },
260
+ ]);
261
+ expect(pretty(state)).toMatchInlineSnapshot(`
262
+ "
263
+ 0: 'a'
264
+ 1: 'b'
265
+ "
266
+ `);
267
+ });
268
+
269
+ it("empty changes is a no-op", () => {
270
+ setup("a", "b");
271
+ apply([]);
272
+ expect(pretty(state)).toMatchInlineSnapshot(`
273
+ "
274
+ 0: 'a'
275
+ 1: 'b'
276
+ "
277
+ `);
278
+ });
279
+ });
@@ -6,6 +6,7 @@ import { EditorView } from "@codemirror/view";
6
6
  import { createStore } from "jotai";
7
7
  import {
8
8
  afterAll,
9
+ afterEach,
9
10
  beforeAll,
10
11
  beforeEach,
11
12
  describe,
@@ -29,6 +30,7 @@ import {
29
30
  type NotebookState,
30
31
  notebookAtom,
31
32
  } from "../cells";
33
+ import { exportedForTesting as documentTransactionTestExports } from "../document-changes";
32
34
  import {
33
35
  focusAndScrollCellIntoView,
34
36
  scrollToBottom,
@@ -147,6 +149,10 @@ describe("cell reducer", () => {
147
149
  firstCellId = state.cellIds.inOrderIds[0];
148
150
  });
149
151
 
152
+ afterEach(() => {
153
+ documentTransactionTestExports.cancelPendingChanges();
154
+ });
155
+
150
156
  afterAll(() => {
151
157
  CellId.create = originalCreate;
152
158
  });