@marimo-team/islands 0.18.2 → 0.18.4
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/{constants-DWBOe162.js → constants-D_G8vnDk.js} +5 -4
- package/dist/{formats-7RSCCoSI.js → formats-Bi_tbdwB.js} +21 -22
- package/dist/{glide-data-editor-D-Ia_Jsv.js → glide-data-editor-DXF8E-QD.js} +2 -2
- package/dist/main.js +280 -148
- package/dist/style.css +1 -1
- package/dist/{types-Dunk85GC.js → types-DclGb0Yh.js} +1 -1
- package/dist/{vega-component-kU4hFYYJ.js → vega-component-BFcH2SqR.js} +8 -8
- package/package.json +1 -1
- package/src/components/app-config/user-config-form.tsx +14 -1
- package/src/components/data-table/context-menu.tsx +7 -3
- package/src/components/data-table/filter-pills.tsx +2 -1
- package/src/components/data-table/filters.ts +11 -2
- package/src/components/editor/cell/CreateCellButton.tsx +5 -3
- package/src/components/editor/cell/collapse.tsx +2 -2
- package/src/components/editor/chrome/components/contribute-snippet-button.tsx +22 -103
- package/src/components/editor/controls/duplicate-shortcut-banner.tsx +50 -0
- package/src/components/editor/controls/keyboard-shortcuts.tsx +25 -2
- package/src/components/editor/notebook-banner.tsx +1 -1
- package/src/components/editor/notebook-cell.tsx +4 -3
- package/src/components/editor/output/__tests__/ansi-reduce.test.ts +6 -6
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +3 -3
- package/src/components/pages/home-page.tsx +6 -0
- package/src/components/scratchpad/scratchpad.tsx +2 -1
- package/src/core/constants.ts +10 -0
- package/src/core/layout/useTogglePresenting.ts +69 -25
- package/src/core/state/__mocks__/mocks.ts +1 -0
- package/src/hooks/__tests__/useDuplicateShortcuts.test.ts +449 -0
- package/src/hooks/useDuplicateShortcuts.ts +145 -0
- package/src/plugins/impl/NumberPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/NumberPlugin.test.tsx +1 -1
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +67 -47
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +2 -57
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +23 -19
- package/src/plugins/impl/anywidget/model.ts +68 -41
- package/src/plugins/impl/data-frames/utils/__tests__/operators.test.ts +2 -0
- package/src/plugins/impl/data-frames/utils/operators.ts +1 -0
- package/src/plugins/impl/vega/vega.css +5 -0
- package/src/plugins/layout/NavigationMenuPlugin.tsx +24 -22
- package/src/plugins/layout/StatPlugin.tsx +43 -23
- package/src/utils/__tests__/data-views.test.ts +495 -13
- package/src/utils/__tests__/json-parser.test.ts +1 -1
- package/src/utils/data-views.ts +134 -16
- package/src/utils/json/base64.ts +8 -0
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
dataViewToBase64,
|
|
5
|
+
decodeFromWire,
|
|
6
|
+
isWireFormat,
|
|
7
|
+
serializeBuffersToBase64,
|
|
8
|
+
type WireFormat,
|
|
9
|
+
} from "../data-views";
|
|
10
|
+
import type { Base64String } from "../json/base64";
|
|
4
11
|
|
|
5
|
-
describe("
|
|
6
|
-
it("should return the original
|
|
7
|
-
const
|
|
8
|
-
const result =
|
|
9
|
-
expect(result).toEqual(
|
|
12
|
+
describe("decodeFromWire with DataViews", () => {
|
|
13
|
+
it("should return the original state if bufferPaths.length === 0", () => {
|
|
14
|
+
const state = { a: 1, b: 2 };
|
|
15
|
+
const result = decodeFromWire({ state, bufferPaths: [] });
|
|
16
|
+
expect(result).toEqual(state);
|
|
10
17
|
});
|
|
11
18
|
|
|
12
|
-
it("should
|
|
13
|
-
const
|
|
19
|
+
it("should insert DataViews at specified buffer paths", () => {
|
|
20
|
+
const state = {
|
|
14
21
|
a: 1,
|
|
15
22
|
b: {
|
|
16
23
|
c: "Hello",
|
|
@@ -25,7 +32,7 @@ describe("updateBufferPaths", () => {
|
|
|
25
32
|
new TextEncoder().encode("Hello"),
|
|
26
33
|
new TextEncoder().encode("World"),
|
|
27
34
|
].map((b) => new DataView(b.buffer));
|
|
28
|
-
const result =
|
|
35
|
+
const result = decodeFromWire({ state, bufferPaths, buffers });
|
|
29
36
|
expect(result).toMatchInlineSnapshot(`
|
|
30
37
|
{
|
|
31
38
|
"a": 1,
|
|
@@ -50,25 +57,500 @@ describe("updateBufferPaths", () => {
|
|
|
50
57
|
});
|
|
51
58
|
|
|
52
59
|
it("should throw error when buffers and paths length mismatch", () => {
|
|
53
|
-
const
|
|
60
|
+
const state = { a: 1 };
|
|
54
61
|
const bufferPaths = [
|
|
55
62
|
["b", "c"],
|
|
56
63
|
["b", "d"],
|
|
57
64
|
];
|
|
58
65
|
const buffers = [new DataView(new ArrayBuffer())]; // Only one buffer for two paths
|
|
59
66
|
|
|
60
|
-
expect(() =>
|
|
67
|
+
expect(() => decodeFromWire({ state, bufferPaths, buffers })).toThrow(
|
|
61
68
|
"Buffers and buffer paths not the same length",
|
|
62
69
|
);
|
|
63
70
|
});
|
|
64
71
|
|
|
65
72
|
it("should handle empty buffers array", () => {
|
|
66
|
-
const
|
|
73
|
+
const state = { a: 1 };
|
|
67
74
|
const bufferPaths = [["b", "c"]];
|
|
68
75
|
const buffers: DataView[] = [];
|
|
69
76
|
|
|
70
|
-
expect(() =>
|
|
77
|
+
expect(() => decodeFromWire({ state, bufferPaths, buffers })).toThrow(
|
|
71
78
|
"Buffers and buffer paths not the same length",
|
|
72
79
|
);
|
|
73
80
|
});
|
|
74
81
|
});
|
|
82
|
+
|
|
83
|
+
describe("Immutability Tests", () => {
|
|
84
|
+
it("serializeBuffersToBase64 should not mutate input", () => {
|
|
85
|
+
const encoder = new TextEncoder();
|
|
86
|
+
const dataView1 = new DataView(encoder.encode("data1").buffer);
|
|
87
|
+
const dataView2 = new DataView(encoder.encode("data2").buffer);
|
|
88
|
+
|
|
89
|
+
const input = {
|
|
90
|
+
buffer1: dataView1,
|
|
91
|
+
nested: {
|
|
92
|
+
buffer2: dataView2,
|
|
93
|
+
value: "test",
|
|
94
|
+
},
|
|
95
|
+
array: [1, 2, 3],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const clone = structuredClone(input);
|
|
99
|
+
|
|
100
|
+
serializeBuffersToBase64(input);
|
|
101
|
+
|
|
102
|
+
// Check deep equality
|
|
103
|
+
expect(input).toEqual(clone);
|
|
104
|
+
|
|
105
|
+
// Check references are unchanged
|
|
106
|
+
expect(input.buffer1).toBe(dataView1);
|
|
107
|
+
expect(input.nested.buffer2).toBe(dataView2);
|
|
108
|
+
expect(input.nested).toBe(input.nested); // Nested object reference unchanged
|
|
109
|
+
expect(input.array).toBe(input.array); // Array reference unchanged
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("decodeFromWire (wire format) should not mutate input", () => {
|
|
113
|
+
const encoder = new TextEncoder();
|
|
114
|
+
const data = encoder.encode("Hello");
|
|
115
|
+
const base64 = btoa(String.fromCharCode(...data)) as Base64String;
|
|
116
|
+
|
|
117
|
+
const input = {
|
|
118
|
+
state: { text: base64, number: 42, nested: { value: "test" } },
|
|
119
|
+
bufferPaths: [["text"]],
|
|
120
|
+
buffers: [base64],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const clone = structuredClone(input);
|
|
124
|
+
|
|
125
|
+
decodeFromWire(input);
|
|
126
|
+
|
|
127
|
+
// Check deep equality
|
|
128
|
+
expect(input).toEqual(clone);
|
|
129
|
+
|
|
130
|
+
// Check references are unchanged
|
|
131
|
+
expect(input.state).toBe(input.state);
|
|
132
|
+
expect(input.state.nested).toBe(input.state.nested);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("decodeFromWire (with DataViews) should not mutate input", () => {
|
|
136
|
+
const encoder = new TextEncoder();
|
|
137
|
+
const dataView = new DataView(encoder.encode("data").buffer);
|
|
138
|
+
|
|
139
|
+
const state = {
|
|
140
|
+
placeholder: "value",
|
|
141
|
+
nested: { key: "test" },
|
|
142
|
+
array: [1, 2, 3],
|
|
143
|
+
};
|
|
144
|
+
const bufferPaths = [["data"]];
|
|
145
|
+
const buffers = [dataView];
|
|
146
|
+
|
|
147
|
+
const input = { state, bufferPaths, buffers };
|
|
148
|
+
const clone = structuredClone(input);
|
|
149
|
+
|
|
150
|
+
decodeFromWire(input);
|
|
151
|
+
|
|
152
|
+
// Check deep equality
|
|
153
|
+
expect(input).toEqual(clone);
|
|
154
|
+
|
|
155
|
+
// Check references are unchanged
|
|
156
|
+
expect(input.state.nested).toBe(state.nested);
|
|
157
|
+
expect(input.state.array).toBe(state.array);
|
|
158
|
+
expect(input.buffers?.[0]).toBe(dataView); // Buffer reference unchanged
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("decodeFromWire should return new object, not mutate", () => {
|
|
162
|
+
const encoder = new TextEncoder();
|
|
163
|
+
const dataView = new DataView(encoder.encode("data").buffer);
|
|
164
|
+
|
|
165
|
+
const state = { a: 1, b: 2 };
|
|
166
|
+
const bufferPaths = [["c"]];
|
|
167
|
+
const buffers = [dataView];
|
|
168
|
+
|
|
169
|
+
const result = decodeFromWire({ state, bufferPaths, buffers });
|
|
170
|
+
|
|
171
|
+
// Result should be different reference
|
|
172
|
+
expect(result).not.toBe(state);
|
|
173
|
+
|
|
174
|
+
// Input should not have new property
|
|
175
|
+
expect("c" in state).toBe(false);
|
|
176
|
+
|
|
177
|
+
// Result should have new property
|
|
178
|
+
expect("c" in result).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("serializeBuffersToBase64 should return new state object", () => {
|
|
182
|
+
const encoder = new TextEncoder();
|
|
183
|
+
const dataView = new DataView(encoder.encode("data").buffer);
|
|
184
|
+
|
|
185
|
+
const input = {
|
|
186
|
+
buffer: dataView,
|
|
187
|
+
value: "test",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const result = serializeBuffersToBase64(input);
|
|
191
|
+
|
|
192
|
+
// State should be different reference
|
|
193
|
+
expect(result.state).not.toBe(input);
|
|
194
|
+
|
|
195
|
+
// Input should still have DataView
|
|
196
|
+
expect(input.buffer).toBeInstanceOf(DataView);
|
|
197
|
+
|
|
198
|
+
// Result state should have base64 string
|
|
199
|
+
expect(typeof result.state.buffer).toBe("string");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("decodeFromWire with object input should not mutate state", () => {
|
|
203
|
+
const encoder = new TextEncoder();
|
|
204
|
+
const dataView = new DataView(encoder.encode("data").buffer);
|
|
205
|
+
|
|
206
|
+
const state = { a: 1, b: { c: 2 } };
|
|
207
|
+
const bufferPaths = [["d"]];
|
|
208
|
+
const buffers = [dataView];
|
|
209
|
+
|
|
210
|
+
const input = { state, bufferPaths, buffers };
|
|
211
|
+
const clone = structuredClone(input);
|
|
212
|
+
|
|
213
|
+
decodeFromWire(input);
|
|
214
|
+
|
|
215
|
+
// Check deep equality
|
|
216
|
+
expect(input).toEqual(clone);
|
|
217
|
+
|
|
218
|
+
// Check references are unchanged
|
|
219
|
+
expect(input.state.b).toBe(state.b);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("nested objects should maintain independence after serialization", () => {
|
|
223
|
+
const encoder = new TextEncoder();
|
|
224
|
+
const dataView = new DataView(encoder.encode("data").buffer);
|
|
225
|
+
|
|
226
|
+
const nested = { buffer: dataView, value: "nested" };
|
|
227
|
+
const input = {
|
|
228
|
+
nested,
|
|
229
|
+
other: "value",
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const result = serializeBuffersToBase64(input);
|
|
233
|
+
|
|
234
|
+
// Mutate result state
|
|
235
|
+
(result.state.nested as Record<string, unknown>).value = "changed";
|
|
236
|
+
|
|
237
|
+
// Original nested object should be unchanged
|
|
238
|
+
expect(nested.value).toBe("nested");
|
|
239
|
+
expect(input.nested.value).toBe("nested");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("arrays should not be mutated during operations", () => {
|
|
243
|
+
const encoder = new TextEncoder();
|
|
244
|
+
const dataView = new DataView(encoder.encode("data").buffer);
|
|
245
|
+
|
|
246
|
+
const input = {
|
|
247
|
+
items: [dataView, "middle", { value: 1 }],
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const originalItems = input.items;
|
|
251
|
+
const originalMiddle = input.items[1];
|
|
252
|
+
const originalObject = input.items[2];
|
|
253
|
+
|
|
254
|
+
serializeBuffersToBase64(input);
|
|
255
|
+
|
|
256
|
+
// Array reference should be unchanged
|
|
257
|
+
expect(input.items).toBe(originalItems);
|
|
258
|
+
expect(input.items[1]).toBe(originalMiddle);
|
|
259
|
+
expect(input.items[2]).toBe(originalObject);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("dataViewToBase64", () => {
|
|
264
|
+
it("should convert a DataView to a base64 string", () => {
|
|
265
|
+
const encoder = new TextEncoder();
|
|
266
|
+
const bytes = encoder.encode("Hello, World!");
|
|
267
|
+
const dataView = new DataView(bytes.buffer);
|
|
268
|
+
const base64 = dataViewToBase64(dataView);
|
|
269
|
+
|
|
270
|
+
// Decode and verify
|
|
271
|
+
const decoded = atob(base64);
|
|
272
|
+
expect(decoded).toBe("Hello, World!");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should handle empty DataView", () => {
|
|
276
|
+
const dataView = new DataView(new ArrayBuffer(0));
|
|
277
|
+
const base64 = dataViewToBase64(dataView);
|
|
278
|
+
expect(base64).toBe("");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should handle DataView with offset and length", () => {
|
|
282
|
+
const encoder = new TextEncoder();
|
|
283
|
+
const bytes = encoder.encode("Hello, World!");
|
|
284
|
+
// Create a DataView that only looks at "World!"
|
|
285
|
+
const dataView = new DataView(bytes.buffer, 7, 6);
|
|
286
|
+
const base64 = dataViewToBase64(dataView);
|
|
287
|
+
|
|
288
|
+
const decoded = atob(base64);
|
|
289
|
+
expect(decoded).toBe("World!");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should handle binary data", () => {
|
|
293
|
+
const bytes = new Uint8Array([0, 1, 2, 255, 254, 253]);
|
|
294
|
+
const dataView = new DataView(bytes.buffer);
|
|
295
|
+
const base64 = dataViewToBase64(dataView);
|
|
296
|
+
|
|
297
|
+
// Verify round-trip
|
|
298
|
+
const decoded = atob(base64);
|
|
299
|
+
const decodedBytes = new Uint8Array(decoded.length);
|
|
300
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
301
|
+
decodedBytes[i] = decoded.charCodeAt(i);
|
|
302
|
+
}
|
|
303
|
+
expect(Array.from(decodedBytes)).toEqual([0, 1, 2, 255, 254, 253]);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("serializeBuffersToBase64", () => {
|
|
308
|
+
it("should return empty arrays when no DataViews present", () => {
|
|
309
|
+
const input = { a: 1, b: "text", c: { d: true } };
|
|
310
|
+
const result = serializeBuffersToBase64(input);
|
|
311
|
+
|
|
312
|
+
expect(result).toEqual({
|
|
313
|
+
state: input,
|
|
314
|
+
buffers: [],
|
|
315
|
+
bufferPaths: [],
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("should serialize DataViews at top level", () => {
|
|
320
|
+
const encoder = new TextEncoder();
|
|
321
|
+
const dataView = new DataView(encoder.encode("test").buffer);
|
|
322
|
+
const input = { data: dataView, other: 123 };
|
|
323
|
+
|
|
324
|
+
const result = serializeBuffersToBase64(input);
|
|
325
|
+
|
|
326
|
+
expect(result.buffers).toHaveLength(1);
|
|
327
|
+
expect(result.bufferPaths).toEqual([["data"]]);
|
|
328
|
+
expect(typeof result.state.data).toBe("string");
|
|
329
|
+
expect(result.state.other).toBe(123);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should serialize nested DataViews", () => {
|
|
333
|
+
const encoder = new TextEncoder();
|
|
334
|
+
const dataView1 = new DataView(encoder.encode("first").buffer);
|
|
335
|
+
const dataView2 = new DataView(encoder.encode("second").buffer);
|
|
336
|
+
const input = {
|
|
337
|
+
nested: {
|
|
338
|
+
buffer1: dataView1,
|
|
339
|
+
deeper: {
|
|
340
|
+
buffer2: dataView2,
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
regular: "value",
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const result = serializeBuffersToBase64(input);
|
|
347
|
+
|
|
348
|
+
expect(result.buffers).toHaveLength(2);
|
|
349
|
+
expect(result.bufferPaths).toEqual([
|
|
350
|
+
["nested", "buffer1"],
|
|
351
|
+
["nested", "deeper", "buffer2"],
|
|
352
|
+
]);
|
|
353
|
+
expect(typeof result.state.nested.buffer1).toBe("string");
|
|
354
|
+
expect(typeof result.state.nested.deeper.buffer2).toBe("string");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should serialize DataViews in arrays", () => {
|
|
358
|
+
const encoder = new TextEncoder();
|
|
359
|
+
const dataView1 = new DataView(encoder.encode("one").buffer);
|
|
360
|
+
const dataView2 = new DataView(encoder.encode("two").buffer);
|
|
361
|
+
const input = {
|
|
362
|
+
items: [dataView1, "middle", dataView2],
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const result = serializeBuffersToBase64(input);
|
|
366
|
+
|
|
367
|
+
expect(result.buffers).toHaveLength(2);
|
|
368
|
+
expect(result.bufferPaths).toEqual([
|
|
369
|
+
["items", 0],
|
|
370
|
+
["items", 2],
|
|
371
|
+
]);
|
|
372
|
+
expect(result.state.items[1]).toBe("middle");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should handle mixed nested structures", () => {
|
|
376
|
+
const encoder = new TextEncoder();
|
|
377
|
+
const dataView = new DataView(encoder.encode("data").buffer);
|
|
378
|
+
const input = {
|
|
379
|
+
array: [{ nested: dataView }, [dataView]],
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const result = serializeBuffersToBase64(input);
|
|
383
|
+
|
|
384
|
+
expect(result.buffers).toHaveLength(2);
|
|
385
|
+
expect(result.bufferPaths).toContainEqual(["array", 0, "nested"]);
|
|
386
|
+
expect(result.bufferPaths).toContainEqual(["array", 1, 0]);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe("isWireFormat", () => {
|
|
391
|
+
it("should return true for valid wire format", () => {
|
|
392
|
+
const wireFormat: WireFormat = {
|
|
393
|
+
state: { a: 1 },
|
|
394
|
+
bufferPaths: [],
|
|
395
|
+
buffers: [],
|
|
396
|
+
};
|
|
397
|
+
expect(isWireFormat(wireFormat)).toBe(true);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("should return true for wire format with data", () => {
|
|
401
|
+
const wireFormat: WireFormat = {
|
|
402
|
+
state: { value: "SGVsbG8=" },
|
|
403
|
+
bufferPaths: [["value"]],
|
|
404
|
+
buffers: ["SGVsbG8=" as Base64String],
|
|
405
|
+
};
|
|
406
|
+
expect(isWireFormat(wireFormat)).toBe(true);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("should return false for null", () => {
|
|
410
|
+
expect(isWireFormat(null)).toBe(false);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("should return false for non-objects", () => {
|
|
414
|
+
expect(isWireFormat("string")).toBe(false);
|
|
415
|
+
expect(isWireFormat(123)).toBe(false);
|
|
416
|
+
expect(isWireFormat(true)).toBe(false);
|
|
417
|
+
expect(isWireFormat(undefined)).toBe(false);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("should return false when missing state", () => {
|
|
421
|
+
expect(isWireFormat({ bufferPaths: [], buffers: [] })).toBe(false);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("should return false when missing bufferPaths", () => {
|
|
425
|
+
expect(isWireFormat({ state: {}, buffers: [] })).toBe(false);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("should return false when missing buffers", () => {
|
|
429
|
+
expect(isWireFormat({ state: {}, bufferPaths: [] })).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("should return false for plain objects", () => {
|
|
433
|
+
expect(isWireFormat({ a: 1, b: 2 })).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe("decodeFromWire from WireFormat", () => {
|
|
438
|
+
it("should return state unchanged when no buffer paths", () => {
|
|
439
|
+
const wire: WireFormat = {
|
|
440
|
+
state: { a: 1, b: "text" },
|
|
441
|
+
bufferPaths: [],
|
|
442
|
+
buffers: [],
|
|
443
|
+
};
|
|
444
|
+
const result = decodeFromWire(wire);
|
|
445
|
+
expect(result).toEqual({ a: 1, b: "text" });
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("should decode single buffer at top level", () => {
|
|
449
|
+
const encoder = new TextEncoder();
|
|
450
|
+
const originalData = encoder.encode("Hello");
|
|
451
|
+
const base64 = btoa(String.fromCharCode(...originalData)) as Base64String;
|
|
452
|
+
|
|
453
|
+
const wire: WireFormat = {
|
|
454
|
+
state: { data: base64, other: 123 },
|
|
455
|
+
bufferPaths: [["data"]],
|
|
456
|
+
buffers: [base64],
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const result = decodeFromWire(wire);
|
|
460
|
+
|
|
461
|
+
expect(result.data).toBeInstanceOf(DataView);
|
|
462
|
+
expect(result.other).toBe(123);
|
|
463
|
+
|
|
464
|
+
// Verify the DataView contains correct data
|
|
465
|
+
const bytes = new Uint8Array((result.data as DataView).buffer);
|
|
466
|
+
const decoded = new TextDecoder().decode(bytes);
|
|
467
|
+
expect(decoded).toBe("Hello");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("should decode nested buffers", () => {
|
|
471
|
+
const encoder = new TextEncoder();
|
|
472
|
+
const data1 = encoder.encode("first");
|
|
473
|
+
const data2 = encoder.encode("second");
|
|
474
|
+
const base641 = btoa(String.fromCharCode(...data1)) as Base64String;
|
|
475
|
+
const base642 = btoa(String.fromCharCode(...data2)) as Base64String;
|
|
476
|
+
|
|
477
|
+
const wire: WireFormat = {
|
|
478
|
+
state: {
|
|
479
|
+
nested: {
|
|
480
|
+
buf1: base641,
|
|
481
|
+
deeper: {
|
|
482
|
+
buf2: base642,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
bufferPaths: [
|
|
487
|
+
["nested", "buf1"],
|
|
488
|
+
["nested", "deeper", "buf2"],
|
|
489
|
+
],
|
|
490
|
+
buffers: [base641, base642],
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const result = decodeFromWire(wire) as {
|
|
494
|
+
nested: { buf1: unknown; deeper: { buf2: unknown } };
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
expect(result.nested.buf1).toBeInstanceOf(DataView);
|
|
498
|
+
expect(result.nested.deeper.buf2).toBeInstanceOf(DataView);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("should decode buffers in arrays", () => {
|
|
502
|
+
const encoder = new TextEncoder();
|
|
503
|
+
const data = encoder.encode("test");
|
|
504
|
+
const base64 = btoa(String.fromCharCode(...data)) as Base64String;
|
|
505
|
+
|
|
506
|
+
const wire: WireFormat = {
|
|
507
|
+
state: {
|
|
508
|
+
items: [base64, "middle", base64],
|
|
509
|
+
},
|
|
510
|
+
bufferPaths: [
|
|
511
|
+
["items", 0],
|
|
512
|
+
["items", 2],
|
|
513
|
+
],
|
|
514
|
+
buffers: [base64, base64],
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const result = decodeFromWire(wire) as { items: unknown[] };
|
|
518
|
+
|
|
519
|
+
expect(result.items[0]).toBeInstanceOf(DataView);
|
|
520
|
+
expect(result.items[1]).toBe("middle");
|
|
521
|
+
expect(result.items[2]).toBeInstanceOf(DataView);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("should handle round-trip serialization", () => {
|
|
525
|
+
const encoder = new TextEncoder();
|
|
526
|
+
const dataView1 = new DataView(encoder.encode("data1").buffer);
|
|
527
|
+
const dataView2 = new DataView(encoder.encode("data2").buffer);
|
|
528
|
+
|
|
529
|
+
const original = {
|
|
530
|
+
buffer1: dataView1,
|
|
531
|
+
nested: {
|
|
532
|
+
buffer2: dataView2,
|
|
533
|
+
},
|
|
534
|
+
regular: "value",
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// Serialize
|
|
538
|
+
const serialized = serializeBuffersToBase64(original);
|
|
539
|
+
|
|
540
|
+
// Deserialize
|
|
541
|
+
const deserialized = decodeFromWire(serialized);
|
|
542
|
+
|
|
543
|
+
// Verify structure is preserved
|
|
544
|
+
expect(deserialized.buffer1).toBeInstanceOf(DataView);
|
|
545
|
+
expect(deserialized.nested.buffer2).toBeInstanceOf(DataView);
|
|
546
|
+
expect(deserialized.regular).toBe("value");
|
|
547
|
+
|
|
548
|
+
// Verify data integrity
|
|
549
|
+
const bytes1 = new Uint8Array((deserialized.buffer1 as DataView).buffer);
|
|
550
|
+
const bytes2 = new Uint8Array(
|
|
551
|
+
(deserialized.nested.buffer2 as DataView).buffer,
|
|
552
|
+
);
|
|
553
|
+
expect(new TextDecoder().decode(bytes1)).toBe("data1");
|
|
554
|
+
expect(new TextDecoder().decode(bytes2)).toBe("data2");
|
|
555
|
+
});
|
|
556
|
+
});
|
|
@@ -113,7 +113,7 @@ it("can convert json to tsv with fr-FR locale", () => {
|
|
|
113
113
|
const locale = "fr-FR";
|
|
114
114
|
|
|
115
115
|
// Handles floats with fr-FR locale (uses , as decimal separator)
|
|
116
|
-
expect(jsonToTSV([{ a: 3.14, b: 2.
|
|
116
|
+
expect(jsonToTSV([{ a: 3.14, b: 2.123_45 }], locale)).toEqual(
|
|
117
117
|
"a\tb\n3,14\t2,12345",
|
|
118
118
|
);
|
|
119
119
|
});
|