@voidhash/mimic 1.0.0-beta.15 → 1.0.0-beta.17
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/Document.cjs +0 -3
- package/dist/Document.d.mts.map +1 -1
- package/dist/Document.mjs +0 -3
- package/dist/Document.mjs.map +1 -1
- package/dist/EffectSchema.cjs +3 -3
- package/dist/EffectSchema.d.cts +5 -5
- package/dist/EffectSchema.d.cts.map +1 -1
- package/dist/EffectSchema.d.mts +5 -5
- package/dist/EffectSchema.d.mts.map +1 -1
- package/dist/EffectSchema.mjs +3 -3
- package/dist/EffectSchema.mjs.map +1 -1
- package/dist/FractionalIndex.mjs.map +1 -1
- package/dist/Operation.d.cts +4 -4
- package/dist/Operation.d.cts.map +1 -1
- package/dist/Operation.d.mts +4 -4
- package/dist/Operation.d.mts.map +1 -1
- package/dist/Operation.mjs.map +1 -1
- package/dist/OperationDefinition.d.cts +2 -2
- package/dist/OperationDefinition.d.cts.map +1 -1
- package/dist/OperationDefinition.d.mts +2 -2
- package/dist/OperationDefinition.d.mts.map +1 -1
- package/dist/OperationDefinition.mjs.map +1 -1
- package/dist/Presence.mjs.map +1 -1
- package/dist/Primitive.d.cts +2 -2
- package/dist/Primitive.d.mts +2 -2
- package/dist/SchemaJSON.cjs +305 -0
- package/dist/SchemaJSON.d.cts +11 -0
- package/dist/SchemaJSON.d.cts.map +1 -0
- package/dist/SchemaJSON.d.mts +11 -0
- package/dist/SchemaJSON.d.mts.map +1 -0
- package/dist/SchemaJSON.mjs +301 -0
- package/dist/SchemaJSON.mjs.map +1 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/primitives/Array.cjs +12 -2
- package/dist/primitives/Array.d.cts.map +1 -1
- package/dist/primitives/Array.d.mts.map +1 -1
- package/dist/primitives/Array.mjs +12 -2
- package/dist/primitives/Array.mjs.map +1 -1
- package/dist/primitives/Boolean.mjs.map +1 -1
- package/dist/primitives/Either.mjs.map +1 -1
- package/dist/primitives/Literal.mjs.map +1 -1
- package/dist/primitives/Number.cjs +27 -5
- package/dist/primitives/Number.d.cts.map +1 -1
- package/dist/primitives/Number.d.mts.map +1 -1
- package/dist/primitives/Number.mjs +27 -5
- package/dist/primitives/Number.mjs.map +1 -1
- package/dist/primitives/String.cjs +44 -13
- package/dist/primitives/String.d.cts.map +1 -1
- package/dist/primitives/String.d.mts.map +1 -1
- package/dist/primitives/String.mjs +44 -13
- package/dist/primitives/String.mjs.map +1 -1
- package/dist/primitives/Struct.cjs +48 -9
- package/dist/primitives/Struct.d.cts +22 -3
- package/dist/primitives/Struct.d.cts.map +1 -1
- package/dist/primitives/Struct.d.mts +22 -3
- package/dist/primitives/Struct.d.mts.map +1 -1
- package/dist/primitives/Struct.mjs +48 -9
- package/dist/primitives/Struct.mjs.map +1 -1
- package/dist/primitives/Union.mjs.map +1 -1
- package/dist/primitives/shared.cjs +2 -5
- package/dist/primitives/shared.d.cts +2 -4
- package/dist/primitives/shared.d.cts.map +1 -1
- package/dist/primitives/shared.d.mts +2 -4
- package/dist/primitives/shared.d.mts.map +1 -1
- package/dist/primitives/shared.mjs +2 -5
- package/dist/primitives/shared.mjs.map +1 -1
- package/package.json +15 -8
- package/src/Document.ts +13 -4
- package/src/EffectSchema.ts +3 -3
- package/src/FractionalIndex.ts +18 -18
- package/src/Operation.ts +5 -5
- package/src/OperationDefinition.ts +2 -2
- package/src/Presence.ts +3 -3
- package/src/SchemaJSON.ts +396 -0
- package/src/index.ts +1 -0
- package/src/primitives/Array.ts +18 -8
- package/src/primitives/Boolean.ts +2 -2
- package/src/primitives/Either.ts +2 -2
- package/src/primitives/Literal.ts +2 -2
- package/src/primitives/Number.ts +44 -22
- package/src/primitives/String.ts +61 -34
- package/src/primitives/Struct.ts +100 -12
- package/src/primitives/Union.ts +1 -1
- package/src/primitives/shared.ts +12 -2
- package/.turbo/turbo-build.log +0 -270
- package/tests/Document.test.ts +0 -557
- package/tests/EffectSchema.test.ts +0 -546
- package/tests/FractionalIndex.test.ts +0 -377
- package/tests/OperationPath.test.ts +0 -151
- package/tests/Presence.test.ts +0 -321
- package/tests/Primitive.test.ts +0 -381
- package/tests/client/ClientDocument.test.ts +0 -1981
- package/tests/client/WebSocketTransport.test.ts +0 -1217
- package/tests/primitives/Array.test.ts +0 -526
- package/tests/primitives/Boolean.test.ts +0 -126
- package/tests/primitives/Either.test.ts +0 -707
- package/tests/primitives/Lazy.test.ts +0 -143
- package/tests/primitives/Literal.test.ts +0 -122
- package/tests/primitives/Number.test.ts +0 -133
- package/tests/primitives/String.test.ts +0 -128
- package/tests/primitives/Struct.test.ts +0 -1044
- package/tests/primitives/Tree.test.ts +0 -1139
- package/tests/primitives/TreeNode.test.ts +0 -50
- package/tests/primitives/Union.test.ts +0 -554
- package/tests/server/ServerDocument.test.ts +0 -903
- package/tsconfig.build.json +0 -24
- package/tsconfig.json +0 -8
- package/tsdown.config.ts +0 -18
- package/vitest.mts +0 -11
|
@@ -1,1139 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "@effect/vitest";
|
|
2
|
-
import * as Primitive from "../../src/Primitive";
|
|
3
|
-
import * as ProxyEnvironment from "../../src/ProxyEnvironment";
|
|
4
|
-
import * as OperationPath from "../../src/OperationPath";
|
|
5
|
-
import * as Operation from "../../src/Operation";
|
|
6
|
-
|
|
7
|
-
const hasOwn = (value: unknown, key: string): boolean =>
|
|
8
|
-
Object.prototype.hasOwnProperty.call(value, key);
|
|
9
|
-
|
|
10
|
-
describe("TreePrimitive", () => {
|
|
11
|
-
// Define node types using the new TreeNode API
|
|
12
|
-
const FileNode = Primitive.TreeNode("file", {
|
|
13
|
-
data: Primitive.Struct({ name: Primitive.String(), size: Primitive.Number() }),
|
|
14
|
-
children: [] as const,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const FolderNode = Primitive.TreeNode("folder", {
|
|
18
|
-
data: Primitive.Struct({ name: Primitive.String() }),
|
|
19
|
-
children: (): readonly Primitive.AnyTreeNodePrimitive[] => [FolderNode, FileNode],
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const fileSystemTree = Primitive.Tree({
|
|
23
|
-
root: FolderNode,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// Helper to create a mock environment with state access
|
|
27
|
-
const createEnvWithState = (
|
|
28
|
-
state: Primitive.TreeState<typeof FolderNode> = []
|
|
29
|
-
): { env: ReturnType<typeof ProxyEnvironment.make>; operations: Operation.Operation<any, any, any>[] } => {
|
|
30
|
-
const operations: Operation.Operation<any, any, any>[] = [];
|
|
31
|
-
let currentState = [...state] as Primitive.TreeState<typeof FolderNode>;
|
|
32
|
-
let idCounter = 0;
|
|
33
|
-
|
|
34
|
-
const env = ProxyEnvironment.make({
|
|
35
|
-
onOperation: (op) => {
|
|
36
|
-
operations.push(op);
|
|
37
|
-
// Apply operation to keep state in sync
|
|
38
|
-
currentState = fileSystemTree._internal.applyOperation(currentState, op);
|
|
39
|
-
},
|
|
40
|
-
getState: () => currentState,
|
|
41
|
-
generateId: () => `node-${++idCounter}`,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
return { env, operations };
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
describe("schema", () => {
|
|
48
|
-
it("exposes root node type", () => {
|
|
49
|
-
expect(fileSystemTree.root).toBe(FolderNode);
|
|
50
|
-
expect(fileSystemTree.root.type).toBe("folder");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("required() returns a new TreePrimitive", () => {
|
|
54
|
-
const required = fileSystemTree.required();
|
|
55
|
-
expect(required).toBeInstanceOf(Primitive.TreePrimitive);
|
|
56
|
-
expect(required).not.toBe(fileSystemTree);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("default() returns a new TreePrimitive with default value", () => {
|
|
60
|
-
const defaultInput = {
|
|
61
|
-
type: "folder" as const,
|
|
62
|
-
id: "root",
|
|
63
|
-
name: "Root",
|
|
64
|
-
children: [],
|
|
65
|
-
};
|
|
66
|
-
const withDefault = fileSystemTree.default(defaultInput);
|
|
67
|
-
const initialState = withDefault._internal.getInitialState();
|
|
68
|
-
expect(initialState).toHaveLength(1);
|
|
69
|
-
expect(initialState![0]!.id).toBe("root");
|
|
70
|
-
expect(initialState![0]!.type).toBe("folder");
|
|
71
|
-
expect(initialState![0]!.data).toEqual({ name: "Root" });
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe("proxy - basic operations", () => {
|
|
76
|
-
it("get() returns empty array for initial state", () => {
|
|
77
|
-
const { env } = createEnvWithState();
|
|
78
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
79
|
-
|
|
80
|
-
expect(proxy.get()).toEqual([]);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("set() generates tree.set operation with nested input converted to flat", () => {
|
|
84
|
-
const { env, operations } = createEnvWithState();
|
|
85
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
86
|
-
|
|
87
|
-
const nestedInput = {
|
|
88
|
-
type: "folder" as const,
|
|
89
|
-
name: "Root",
|
|
90
|
-
children: [],
|
|
91
|
-
};
|
|
92
|
-
proxy.set(nestedInput);
|
|
93
|
-
|
|
94
|
-
expect(operations).toHaveLength(1);
|
|
95
|
-
expect(operations[0]!.kind).toBe("tree.set");
|
|
96
|
-
// The payload should be flat format
|
|
97
|
-
const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
|
|
98
|
-
expect(payload).toHaveLength(1);
|
|
99
|
-
expect(payload[0]!.type).toBe("folder");
|
|
100
|
-
expect(payload[0]!.data).toEqual({ name: "Root" });
|
|
101
|
-
expect(payload[0]!.parentId).toBe(null);
|
|
102
|
-
expect(payload[0]!.id).toBe("node-1"); // Generated by env.generateId
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("root() returns the root node", () => {
|
|
106
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
107
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
108
|
-
{ id: "child1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
109
|
-
];
|
|
110
|
-
const { env } = createEnvWithState(initialState);
|
|
111
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
112
|
-
|
|
113
|
-
const root = proxy.root();
|
|
114
|
-
expect(root).toBeDefined();
|
|
115
|
-
expect(root!.id).toBe("root");
|
|
116
|
-
expect(root!.parentId).toBe(null);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("node() returns a node proxy by ID", () => {
|
|
120
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
121
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
122
|
-
{ id: "child1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
123
|
-
];
|
|
124
|
-
const { env } = createEnvWithState(initialState);
|
|
125
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
126
|
-
|
|
127
|
-
const node = proxy.node("child1");
|
|
128
|
-
expect(node).toBeDefined();
|
|
129
|
-
expect(node!.id).toBe("child1");
|
|
130
|
-
expect(node!.type).toBe("file");
|
|
131
|
-
expect(node!.get().data).toEqual({ name: "File1", size: 100 });
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("children() returns ordered children", () => {
|
|
135
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
136
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
137
|
-
{ id: "child2", type: "file", parentId: "root", pos: "a1", data: { name: "File2", size: 200 } },
|
|
138
|
-
{ id: "child1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
139
|
-
];
|
|
140
|
-
const { env } = createEnvWithState(initialState);
|
|
141
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
142
|
-
|
|
143
|
-
const children = proxy.children("root");
|
|
144
|
-
expect(children).toHaveLength(2);
|
|
145
|
-
expect(children[0]!.id).toBe("child1"); // a0 comes first
|
|
146
|
-
expect(children[1]!.id).toBe("child2"); // a1 comes second
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe("proxy - type narrowing with is() and as()", () => {
|
|
151
|
-
it("is() returns true for matching node type", () => {
|
|
152
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
153
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
154
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
155
|
-
];
|
|
156
|
-
const { env } = createEnvWithState(initialState);
|
|
157
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
158
|
-
|
|
159
|
-
const fileProxy = proxy.node("file1");
|
|
160
|
-
expect(fileProxy!.is(FileNode)).toBe(true);
|
|
161
|
-
expect(fileProxy!.is(FolderNode)).toBe(false);
|
|
162
|
-
|
|
163
|
-
const folderProxy = proxy.node("root");
|
|
164
|
-
expect(folderProxy!.is(FolderNode)).toBe(true);
|
|
165
|
-
expect(folderProxy!.is(FileNode)).toBe(false);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("as() returns typed proxy for correct type", () => {
|
|
169
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
170
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
171
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
172
|
-
];
|
|
173
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
174
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
175
|
-
|
|
176
|
-
const fileProxy = proxy.node("file1")!.as(FileNode);
|
|
177
|
-
expect(fileProxy.id).toBe("file1");
|
|
178
|
-
expect(fileProxy.type).toBe("file");
|
|
179
|
-
|
|
180
|
-
// Type-safe data access
|
|
181
|
-
fileProxy.data.name.set("UpdatedName");
|
|
182
|
-
expect(operations).toHaveLength(1);
|
|
183
|
-
expect(operations[0]!.kind).toBe("string.set");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("as() throws for wrong type", () => {
|
|
187
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
188
|
-
{ id: "file1", type: "file", parentId: null, pos: "a0", data: { name: "File1", size: 100 } },
|
|
189
|
-
];
|
|
190
|
-
const { env } = createEnvWithState(initialState);
|
|
191
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
192
|
-
|
|
193
|
-
const nodeProxy = proxy.node("file1");
|
|
194
|
-
expect(() => nodeProxy!.as(FolderNode)).toThrow(Primitive.ValidationError);
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe("proxy - insert operations with TreeNode types", () => {
|
|
199
|
-
it("insertFirst() creates node at beginning of children", () => {
|
|
200
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
201
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
202
|
-
{ id: "existing", type: "file", parentId: "root", pos: "a1", data: { name: "Existing", size: 100 } },
|
|
203
|
-
];
|
|
204
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
205
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
206
|
-
|
|
207
|
-
const newId = proxy.insertFirst("root", FileNode, { name: "First", size: 50 });
|
|
208
|
-
|
|
209
|
-
expect(operations).toHaveLength(1);
|
|
210
|
-
expect(operations[0]!.kind).toBe("tree.insert");
|
|
211
|
-
expect(newId).toBe("node-1");
|
|
212
|
-
|
|
213
|
-
const payload = operations[0]!.payload as { id: string; pos: string; type: string };
|
|
214
|
-
expect(payload.type).toBe("file");
|
|
215
|
-
expect(payload.pos < "a1").toBe(true); // Should be before existing
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("insertLast() creates node at end of children", () => {
|
|
219
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
220
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
221
|
-
{ id: "existing", type: "file", parentId: "root", pos: "a0", data: { name: "Existing", size: 100 } },
|
|
222
|
-
];
|
|
223
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
224
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
225
|
-
|
|
226
|
-
proxy.insertLast("root", FileNode, { name: "Last", size: 50 });
|
|
227
|
-
|
|
228
|
-
const payload = operations[0]!.payload as { pos: string };
|
|
229
|
-
expect(payload.pos > "a0").toBe(true); // Should be after existing
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("insertFirst() with null parentId creates root node", () => {
|
|
233
|
-
const { env, operations } = createEnvWithState();
|
|
234
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
235
|
-
|
|
236
|
-
proxy.insertFirst(null, FolderNode, { name: "Root" });
|
|
237
|
-
|
|
238
|
-
expect(operations).toHaveLength(1);
|
|
239
|
-
const payload = operations[0]!.payload as { parentId: string | null; type: string };
|
|
240
|
-
expect(payload.parentId).toBe(null);
|
|
241
|
-
expect(payload.type).toBe("folder");
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe("proxy - validation", () => {
|
|
246
|
-
it("throws when inserting invalid child type", () => {
|
|
247
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
248
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
249
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
250
|
-
];
|
|
251
|
-
const { env } = createEnvWithState(initialState);
|
|
252
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
253
|
-
|
|
254
|
-
// Files cannot have children
|
|
255
|
-
expect(() => proxy.insertFirst("file1", FileNode, { name: "Child", size: 50 })).toThrow(
|
|
256
|
-
Primitive.ValidationError
|
|
257
|
-
);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("throws when inserting non-root type at root level", () => {
|
|
261
|
-
const { env } = createEnvWithState();
|
|
262
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
263
|
-
|
|
264
|
-
expect(() => proxy.insertFirst(null, FileNode, { name: "File", size: 50 })).toThrow(
|
|
265
|
-
Primitive.ValidationError
|
|
266
|
-
);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("throws when inserting second root", () => {
|
|
270
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
271
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
272
|
-
];
|
|
273
|
-
const { env } = createEnvWithState(initialState);
|
|
274
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
275
|
-
|
|
276
|
-
expect(() => proxy.insertFirst(null, FolderNode, { name: "SecondRoot" })).toThrow(
|
|
277
|
-
Primitive.ValidationError
|
|
278
|
-
);
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
describe("proxy - toSnapshot()", () => {
|
|
283
|
-
it("returns undefined for empty tree", () => {
|
|
284
|
-
const { env } = createEnvWithState();
|
|
285
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
286
|
-
|
|
287
|
-
expect(proxy.toSnapshot()).toBeUndefined();
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("returns nested snapshot with spread data", () => {
|
|
291
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
292
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
293
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
294
|
-
{ id: "folder1", type: "folder", parentId: "root", pos: "a1", data: { name: "Subfolder" } },
|
|
295
|
-
{ id: "file2", type: "file", parentId: "folder1", pos: "a0", data: { name: "File2", size: 200 } },
|
|
296
|
-
];
|
|
297
|
-
const { env } = createEnvWithState(initialState);
|
|
298
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const snapshot = proxy.toSnapshot();
|
|
302
|
-
expect(snapshot).toBeDefined();
|
|
303
|
-
expect(snapshot!.id).toBe("root");
|
|
304
|
-
expect(snapshot!.type).toBe("folder");
|
|
305
|
-
expect(snapshot!.parentId).toBe(null);
|
|
306
|
-
expect(snapshot!.pos).toBe("a0");
|
|
307
|
-
expect(snapshot!.name).toBe("Root"); // Data spread at node level
|
|
308
|
-
expect(snapshot!.children).toHaveLength(2);
|
|
309
|
-
|
|
310
|
-
const file1Snapshot = snapshot!.children[0]!;
|
|
311
|
-
expect(file1Snapshot.id).toBe("file1");
|
|
312
|
-
expect(file1Snapshot.parentId).toBe("root");
|
|
313
|
-
expect(file1Snapshot.pos).toBe("a0");
|
|
314
|
-
expect(file1Snapshot.name).toBe("File1");
|
|
315
|
-
expect(file1Snapshot.children).toEqual([]);
|
|
316
|
-
|
|
317
|
-
const folder1Snapshot = snapshot!.children[1]!;
|
|
318
|
-
expect(folder1Snapshot.id).toBe("folder1");
|
|
319
|
-
expect(folder1Snapshot.children).toHaveLength(1);
|
|
320
|
-
expect(folder1Snapshot.children[0]!.name).toBe("File2");
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
describe("proxy - at() with typed node", () => {
|
|
325
|
-
it("at() returns typed proxy for node data", () => {
|
|
326
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
327
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
328
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
329
|
-
];
|
|
330
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
331
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
332
|
-
|
|
333
|
-
proxy.at("file1", FileNode).name.set("UpdatedName");
|
|
334
|
-
|
|
335
|
-
expect(operations).toHaveLength(1);
|
|
336
|
-
expect(operations[0]!.kind).toBe("string.set");
|
|
337
|
-
expect(operations[0]!.path.toTokens()).toEqual(["file1", "name"]);
|
|
338
|
-
expect(operations[0]!.payload).toBe("UpdatedName");
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it("at() throws when node type mismatch", () => {
|
|
342
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
343
|
-
{ id: "file1", type: "file", parentId: null, pos: "a0", data: { name: "File1", size: 100 } },
|
|
344
|
-
];
|
|
345
|
-
const { env } = createEnvWithState(initialState);
|
|
346
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
347
|
-
|
|
348
|
-
expect(() => proxy.at("file1", FolderNode)).toThrow(Primitive.ValidationError);
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
describe("proxy - move operations", () => {
|
|
353
|
-
it("move() changes parent and position", () => {
|
|
354
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
355
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
356
|
-
{ id: "folder1", type: "folder", parentId: "root", pos: "a0", data: { name: "Folder1" } },
|
|
357
|
-
{ id: "folder2", type: "folder", parentId: "root", pos: "a1", data: { name: "Folder2" } },
|
|
358
|
-
{ id: "file1", type: "file", parentId: "folder1", pos: "a0", data: { name: "File1", size: 100 } },
|
|
359
|
-
];
|
|
360
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
361
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
362
|
-
|
|
363
|
-
// Move file1 to folder2
|
|
364
|
-
proxy.move("file1", "folder2", 0);
|
|
365
|
-
|
|
366
|
-
expect(operations).toHaveLength(1);
|
|
367
|
-
expect(operations[0]!.kind).toBe("tree.move");
|
|
368
|
-
const payload = operations[0]!.payload as { id: string; parentId: string };
|
|
369
|
-
expect(payload.id).toBe("file1");
|
|
370
|
-
expect(payload.parentId).toBe("folder2");
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it("throws when moving node to its descendant (cycle prevention)", () => {
|
|
374
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
375
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
376
|
-
{ id: "folder1", type: "folder", parentId: "root", pos: "a0", data: { name: "Folder1" } },
|
|
377
|
-
{ id: "folder2", type: "folder", parentId: "folder1", pos: "a0", data: { name: "Folder2" } },
|
|
378
|
-
];
|
|
379
|
-
const { env } = createEnvWithState(initialState);
|
|
380
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
381
|
-
|
|
382
|
-
expect(() => proxy.move("folder1", "folder2", 0)).toThrow(Primitive.ValidationError);
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
describe("applyOperation", () => {
|
|
387
|
-
it("tree.set replaces entire tree", () => {
|
|
388
|
-
const newNodes: Primitive.TreeState<typeof FolderNode> = [
|
|
389
|
-
{ id: "new-root", type: "folder", parentId: null, pos: "a0", data: { name: "NewRoot" } },
|
|
390
|
-
];
|
|
391
|
-
const operation: Operation.Operation<any, any, any> = {
|
|
392
|
-
kind: "tree.set",
|
|
393
|
-
path: OperationPath.make(""),
|
|
394
|
-
payload: newNodes,
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
const result = fileSystemTree._internal.applyOperation([], operation);
|
|
398
|
-
expect(result).toEqual(newNodes);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it("tree.insert adds a new node", () => {
|
|
402
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
403
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
404
|
-
];
|
|
405
|
-
const operation: Operation.Operation<any, any, any> = {
|
|
406
|
-
kind: "tree.insert",
|
|
407
|
-
path: OperationPath.make(""),
|
|
408
|
-
payload: {
|
|
409
|
-
id: "file1",
|
|
410
|
-
type: "file",
|
|
411
|
-
parentId: "root",
|
|
412
|
-
pos: "a0",
|
|
413
|
-
data: { name: "File1", size: 100 },
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const result = fileSystemTree._internal.applyOperation(initialState, operation);
|
|
418
|
-
expect(result).toHaveLength(2);
|
|
419
|
-
expect(result[1]).toEqual({
|
|
420
|
-
id: "file1",
|
|
421
|
-
type: "file",
|
|
422
|
-
parentId: "root",
|
|
423
|
-
pos: "a0",
|
|
424
|
-
data: { name: "File1", size: 100 },
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it("tree.remove removes node and descendants", () => {
|
|
429
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
430
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
431
|
-
{ id: "folder1", type: "folder", parentId: "root", pos: "a0", data: { name: "Folder1" } },
|
|
432
|
-
{ id: "file1", type: "file", parentId: "folder1", pos: "a0", data: { name: "File1", size: 100 } },
|
|
433
|
-
{ id: "folder2", type: "folder", parentId: "root", pos: "a1", data: { name: "Folder2" } },
|
|
434
|
-
];
|
|
435
|
-
const operation: Operation.Operation<any, any, any> = {
|
|
436
|
-
kind: "tree.remove",
|
|
437
|
-
path: OperationPath.make(""),
|
|
438
|
-
payload: { id: "folder1" },
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
const result = fileSystemTree._internal.applyOperation(initialState, operation);
|
|
442
|
-
expect(result).toHaveLength(2);
|
|
443
|
-
expect(result.map(n => n.id)).toEqual(["root", "folder2"]);
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it("delegates node data operations", () => {
|
|
447
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
448
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
449
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
450
|
-
];
|
|
451
|
-
const operation: Operation.Operation<any, any, any> = {
|
|
452
|
-
kind: "string.set",
|
|
453
|
-
path: OperationPath.make("file1/name"),
|
|
454
|
-
payload: "UpdatedName",
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
const result = fileSystemTree._internal.applyOperation(initialState, operation);
|
|
458
|
-
const updatedNode = result.find(n => n.id === "file1");
|
|
459
|
-
expect(updatedNode!.data).toEqual({ name: "UpdatedName", size: 100 });
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
describe("getInitialState", () => {
|
|
464
|
-
it("automatically creates a root node when no default is set", () => {
|
|
465
|
-
const initialState = fileSystemTree._internal.getInitialState();
|
|
466
|
-
expect(initialState).toBeDefined();
|
|
467
|
-
expect(initialState).toHaveLength(1);
|
|
468
|
-
expect(initialState![0]).toMatchObject({
|
|
469
|
-
type: "folder",
|
|
470
|
-
parentId: null,
|
|
471
|
-
data: {},
|
|
472
|
-
});
|
|
473
|
-
// Verify ID and pos are generated
|
|
474
|
-
expect(typeof initialState![0]!.id).toBe("string");
|
|
475
|
-
expect(typeof initialState![0]!.pos).toBe("string");
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
it("returns the default value when set (converted from nested)", () => {
|
|
479
|
-
const defaultInput = {
|
|
480
|
-
type: "folder" as const,
|
|
481
|
-
id: "root",
|
|
482
|
-
name: "Root",
|
|
483
|
-
children: [],
|
|
484
|
-
};
|
|
485
|
-
const withDefault = fileSystemTree.default(defaultInput);
|
|
486
|
-
const initialState = withDefault._internal.getInitialState();
|
|
487
|
-
expect(initialState).toHaveLength(1);
|
|
488
|
-
expect(initialState![0]!.id).toBe("root");
|
|
489
|
-
expect(initialState![0]!.type).toBe("folder");
|
|
490
|
-
expect(initialState![0]!.parentId).toBe(null);
|
|
491
|
-
expect(initialState![0]!.data).toEqual({ name: "Root" });
|
|
492
|
-
});
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
describe("proxy - partial update", () => {
|
|
496
|
-
it("update() on TypedNodeProxy updates only specified fields", () => {
|
|
497
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
498
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
499
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
500
|
-
];
|
|
501
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
502
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
503
|
-
|
|
504
|
-
// Use the update method via as()
|
|
505
|
-
const fileProxy = proxy.node("file1")!.as(FileNode);
|
|
506
|
-
fileProxy.update({ name: "UpdatedName" });
|
|
507
|
-
|
|
508
|
-
// Should generate only a string.set operation for the name field
|
|
509
|
-
expect(operations).toHaveLength(1);
|
|
510
|
-
expect(operations[0]!.kind).toBe("string.set");
|
|
511
|
-
expect(operations[0]!.path.toTokens()).toEqual(["file1", "name"]);
|
|
512
|
-
expect(operations[0]!.payload).toBe("UpdatedName");
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
it("update() preserves other fields when updating partial data", () => {
|
|
516
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
517
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
518
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
519
|
-
];
|
|
520
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
521
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
522
|
-
|
|
523
|
-
// Update only the size field
|
|
524
|
-
proxy.node("file1")!.as(FileNode).update({ size: 200 });
|
|
525
|
-
|
|
526
|
-
// Should generate only a number.set operation for the size field
|
|
527
|
-
expect(operations).toHaveLength(1);
|
|
528
|
-
expect(operations[0]!.kind).toBe("number.set");
|
|
529
|
-
expect(operations[0]!.path.toTokens()).toEqual(["file1", "size"]);
|
|
530
|
-
expect(operations[0]!.payload).toBe(200);
|
|
531
|
-
|
|
532
|
-
// The name should remain unchanged in the state
|
|
533
|
-
const updatedState = proxy.get();
|
|
534
|
-
const file1 = updatedState.find(n => n.id === "file1");
|
|
535
|
-
expect(file1!.data).toEqual({ name: "File1", size: 200 });
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it("update() handles multiple fields at once", () => {
|
|
539
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
540
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
541
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
542
|
-
];
|
|
543
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
544
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
545
|
-
|
|
546
|
-
// Update both name and size
|
|
547
|
-
proxy.node("file1")!.as(FileNode).update({ name: "NewFile", size: 500 });
|
|
548
|
-
|
|
549
|
-
// Should generate two operations
|
|
550
|
-
expect(operations).toHaveLength(2);
|
|
551
|
-
|
|
552
|
-
// Verify both fields were updated
|
|
553
|
-
const updatedState = proxy.get();
|
|
554
|
-
const file1 = updatedState.find(n => n.id === "file1");
|
|
555
|
-
expect(file1!.data).toEqual({ name: "NewFile", size: 500 });
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
it("updateAt() provides convenient partial update by node id", () => {
|
|
559
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
560
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
561
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
562
|
-
];
|
|
563
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
564
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
565
|
-
|
|
566
|
-
// Use updateAt for convenience
|
|
567
|
-
proxy.updateAt("file1", FileNode, { name: "QuickUpdate" });
|
|
568
|
-
|
|
569
|
-
expect(operations).toHaveLength(1);
|
|
570
|
-
expect(operations[0]!.kind).toBe("string.set");
|
|
571
|
-
expect(operations[0]!.path.toTokens()).toEqual(["file1", "name"]);
|
|
572
|
-
expect(operations[0]!.payload).toBe("QuickUpdate");
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it("updateAt() throws for wrong node type", () => {
|
|
576
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
577
|
-
{ id: "file1", type: "file", parentId: null, pos: "a0", data: { name: "File1", size: 100 } },
|
|
578
|
-
];
|
|
579
|
-
const { env } = createEnvWithState(initialState);
|
|
580
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
581
|
-
|
|
582
|
-
// Trying to update a file node as a folder should throw
|
|
583
|
-
expect(() => proxy.updateAt("file1", FolderNode, { name: "NewName" })).toThrow(
|
|
584
|
-
Primitive.ValidationError
|
|
585
|
-
);
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
it("updateAt() throws for non-existent node", () => {
|
|
589
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
590
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
591
|
-
];
|
|
592
|
-
const { env } = createEnvWithState(initialState);
|
|
593
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
594
|
-
|
|
595
|
-
expect(() => proxy.updateAt("nonexistent", FileNode, { name: "Name" })).toThrow(
|
|
596
|
-
Primitive.ValidationError
|
|
597
|
-
);
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
it("data.update() on at() proxy also works for partial updates", () => {
|
|
601
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
602
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
603
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
604
|
-
];
|
|
605
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
606
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
607
|
-
|
|
608
|
-
// The at() method returns the data proxy which has update()
|
|
609
|
-
proxy.at("file1", FileNode).update({ size: 999 });
|
|
610
|
-
|
|
611
|
-
expect(operations).toHaveLength(1);
|
|
612
|
-
expect(operations[0]!.kind).toBe("number.set");
|
|
613
|
-
expect(operations[0]!.path.toTokens()).toEqual(["file1", "size"]);
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
it("update() removes optional node data key when value is undefined", () => {
|
|
617
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
618
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
619
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
620
|
-
];
|
|
621
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
622
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
623
|
-
|
|
624
|
-
proxy.node("file1")!.as(FileNode).update({ size: undefined });
|
|
625
|
-
|
|
626
|
-
expect(operations).toHaveLength(1);
|
|
627
|
-
expect(operations[0]!.kind).toBe("struct.unset");
|
|
628
|
-
expect(operations[0]!.path.toTokens()).toEqual(["file1", "size"]);
|
|
629
|
-
|
|
630
|
-
const file1 = proxy.get().find((n) => n.id === "file1")!;
|
|
631
|
-
expect(file1.data).toEqual({ name: "File1" });
|
|
632
|
-
expect(hasOwn(file1.data, "size")).toBe(false);
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
it("updateAt() removes optional node data key when value is null", () => {
|
|
636
|
-
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
637
|
-
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
638
|
-
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
639
|
-
];
|
|
640
|
-
const { env, operations } = createEnvWithState(initialState);
|
|
641
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
642
|
-
|
|
643
|
-
(proxy as any).updateAt("file1", FileNode, { size: null });
|
|
644
|
-
|
|
645
|
-
expect(operations).toHaveLength(1);
|
|
646
|
-
expect(operations[0]!.kind).toBe("struct.unset");
|
|
647
|
-
expect(operations[0]!.path.toTokens()).toEqual(["file1", "size"]);
|
|
648
|
-
|
|
649
|
-
const file1 = proxy.get().find((n) => n.id === "file1")!;
|
|
650
|
-
expect(file1.data).toEqual({ name: "File1" });
|
|
651
|
-
expect(hasOwn(file1.data, "size")).toBe(false);
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
it("throws when required node data field is updated with undefined", () => {
|
|
655
|
-
const StrictFileNode = Primitive.TreeNode("strict-file", {
|
|
656
|
-
data: Primitive.Struct({
|
|
657
|
-
name: Primitive.String().required(),
|
|
658
|
-
note: Primitive.String(),
|
|
659
|
-
}),
|
|
660
|
-
children: [] as const,
|
|
661
|
-
});
|
|
662
|
-
const StrictFolderNode = Primitive.TreeNode("strict-folder", {
|
|
663
|
-
data: Primitive.Struct({ label: Primitive.String() }),
|
|
664
|
-
children: [StrictFileNode] as const,
|
|
665
|
-
});
|
|
666
|
-
const strictTree = Primitive.Tree({ root: StrictFolderNode });
|
|
667
|
-
const initialState: Primitive.TreeState<typeof StrictFolderNode> = [
|
|
668
|
-
{ id: "root", type: "strict-folder", parentId: null, pos: "a0", data: { label: "Root" } },
|
|
669
|
-
{ id: "file1", type: "strict-file", parentId: "root", pos: "a0", data: { name: "File1", note: "keep" } },
|
|
670
|
-
];
|
|
671
|
-
const operations: Operation.Operation<any, any, any>[] = [];
|
|
672
|
-
const env = ProxyEnvironment.make({
|
|
673
|
-
onOperation: (op) => {
|
|
674
|
-
operations.push(op);
|
|
675
|
-
},
|
|
676
|
-
getState: () => initialState,
|
|
677
|
-
});
|
|
678
|
-
const proxy = strictTree._internal.createProxy(env, OperationPath.make(""));
|
|
679
|
-
|
|
680
|
-
expect(() => proxy.node("file1")!.as(StrictFileNode).update({ name: undefined as never })).toThrow(
|
|
681
|
-
Primitive.ValidationError
|
|
682
|
-
);
|
|
683
|
-
expect(operations).toHaveLength(0);
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
it("throws when required node data field is updated with null", () => {
|
|
687
|
-
const StrictFileNode = Primitive.TreeNode("strict-file", {
|
|
688
|
-
data: Primitive.Struct({
|
|
689
|
-
name: Primitive.String().required(),
|
|
690
|
-
note: Primitive.String(),
|
|
691
|
-
}),
|
|
692
|
-
children: [] as const,
|
|
693
|
-
});
|
|
694
|
-
const StrictFolderNode = Primitive.TreeNode("strict-folder", {
|
|
695
|
-
data: Primitive.Struct({ label: Primitive.String() }),
|
|
696
|
-
children: [StrictFileNode] as const,
|
|
697
|
-
});
|
|
698
|
-
const strictTree = Primitive.Tree({ root: StrictFolderNode });
|
|
699
|
-
const initialState: Primitive.TreeState<typeof StrictFolderNode> = [
|
|
700
|
-
{ id: "root", type: "strict-folder", parentId: null, pos: "a0", data: { label: "Root" } },
|
|
701
|
-
{ id: "file1", type: "strict-file", parentId: "root", pos: "a0", data: { name: "File1", note: "keep" } },
|
|
702
|
-
];
|
|
703
|
-
const operations: Operation.Operation<any, any, any>[] = [];
|
|
704
|
-
const env = ProxyEnvironment.make({
|
|
705
|
-
onOperation: (op) => {
|
|
706
|
-
operations.push(op);
|
|
707
|
-
},
|
|
708
|
-
getState: () => initialState,
|
|
709
|
-
});
|
|
710
|
-
const proxy = strictTree._internal.createProxy(env, OperationPath.make(""));
|
|
711
|
-
|
|
712
|
-
expect(() => (proxy as any).updateAt("file1", StrictFileNode, { name: null })).toThrow(
|
|
713
|
-
Primitive.ValidationError
|
|
714
|
-
);
|
|
715
|
-
expect(operations).toHaveLength(0);
|
|
716
|
-
});
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
describe("proxy - insert with defaults", () => {
|
|
720
|
-
// Define node types with defaults
|
|
721
|
-
const ItemNodeWithDefaults = Primitive.TreeNode("item", {
|
|
722
|
-
data: Primitive.Struct({
|
|
723
|
-
title: Primitive.String().required(), // Must provide
|
|
724
|
-
count: Primitive.Number().default(0), // Has default, optional
|
|
725
|
-
active: Primitive.Boolean().default(true), // Has default, optional
|
|
726
|
-
}),
|
|
727
|
-
children: [] as const,
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
const ContainerNodeWithDefaults = Primitive.TreeNode("container", {
|
|
731
|
-
data: Primitive.Struct({
|
|
732
|
-
name: Primitive.String().required(), // Must provide
|
|
733
|
-
}),
|
|
734
|
-
children: (): readonly Primitive.AnyTreeNodePrimitive[] => [ContainerNodeWithDefaults, ItemNodeWithDefaults],
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
const treeWithDefaults = Primitive.Tree({
|
|
738
|
-
root: ContainerNodeWithDefaults,
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
// Helper to create a mock environment
|
|
742
|
-
const createEnvWithDefaultsTree = (
|
|
743
|
-
state: Primitive.TreeState<typeof ContainerNodeWithDefaults> = []
|
|
744
|
-
): { env: ReturnType<typeof ProxyEnvironment.make>; operations: Operation.Operation<any, any, any>[] } => {
|
|
745
|
-
const operations: Operation.Operation<any, any, any>[] = [];
|
|
746
|
-
let currentState = [...state] as Primitive.TreeState<typeof ContainerNodeWithDefaults>;
|
|
747
|
-
let idCounter = 0;
|
|
748
|
-
|
|
749
|
-
const env = ProxyEnvironment.make({
|
|
750
|
-
onOperation: (op) => {
|
|
751
|
-
operations.push(op);
|
|
752
|
-
currentState = treeWithDefaults._internal.applyOperation(currentState, op);
|
|
753
|
-
},
|
|
754
|
-
getState: () => currentState,
|
|
755
|
-
generateId: () => `node-${++idCounter}`,
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
return { env, operations };
|
|
759
|
-
};
|
|
760
|
-
|
|
761
|
-
it("insertFirst() only requires fields without defaults", () => {
|
|
762
|
-
const initialState: Primitive.TreeState<typeof ContainerNodeWithDefaults> = [
|
|
763
|
-
{ id: "root", type: "container", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
764
|
-
];
|
|
765
|
-
const { env, operations } = createEnvWithDefaultsTree(initialState);
|
|
766
|
-
const proxy = treeWithDefaults._internal.createProxy(env, OperationPath.make(""));
|
|
767
|
-
|
|
768
|
-
// Only provide required field 'title', count and active should use defaults
|
|
769
|
-
const newId = proxy.insertFirst("root", ItemNodeWithDefaults, { title: "New Item" });
|
|
770
|
-
|
|
771
|
-
expect(operations).toHaveLength(1);
|
|
772
|
-
expect(operations[0]!.kind).toBe("tree.insert");
|
|
773
|
-
expect(newId).toBe("node-1");
|
|
774
|
-
|
|
775
|
-
const payload = operations[0]!.payload as { data: { title: string; count: number; active: boolean } };
|
|
776
|
-
expect(payload.data.title).toBe("New Item");
|
|
777
|
-
expect(payload.data.count).toBe(0); // Default value
|
|
778
|
-
expect(payload.data.active).toBe(true); // Default value
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
it("insertLast() applies defaults for omitted fields", () => {
|
|
782
|
-
const initialState: Primitive.TreeState<typeof ContainerNodeWithDefaults> = [
|
|
783
|
-
{ id: "root", type: "container", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
784
|
-
];
|
|
785
|
-
const { env, operations } = createEnvWithDefaultsTree(initialState);
|
|
786
|
-
const proxy = treeWithDefaults._internal.createProxy(env, OperationPath.make(""));
|
|
787
|
-
|
|
788
|
-
// Provide title and override count, let active use default
|
|
789
|
-
proxy.insertLast("root", ItemNodeWithDefaults, { title: "Item", count: 42 });
|
|
790
|
-
|
|
791
|
-
const payload = operations[0]!.payload as { data: { title: string; count: number; active: boolean } };
|
|
792
|
-
expect(payload.data.title).toBe("Item");
|
|
793
|
-
expect(payload.data.count).toBe(42); // Overridden
|
|
794
|
-
expect(payload.data.active).toBe(true); // Default value
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
it("insertAt() allows omitting all optional fields with defaults", () => {
|
|
798
|
-
const initialState: Primitive.TreeState<typeof ContainerNodeWithDefaults> = [
|
|
799
|
-
{ id: "root", type: "container", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
800
|
-
];
|
|
801
|
-
const { env, operations } = createEnvWithDefaultsTree(initialState);
|
|
802
|
-
const proxy = treeWithDefaults._internal.createProxy(env, OperationPath.make(""));
|
|
803
|
-
|
|
804
|
-
// Only provide required 'name' for container
|
|
805
|
-
proxy.insertAt("root", 0, ContainerNodeWithDefaults, { name: "Subfolder" });
|
|
806
|
-
|
|
807
|
-
const payload = operations[0]!.payload as { type: string; data: { name: string } };
|
|
808
|
-
expect(payload.type).toBe("container");
|
|
809
|
-
expect(payload.data.name).toBe("Subfolder");
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
it("insertAfter() uses defaults when fields are omitted", () => {
|
|
813
|
-
const initialState: Primitive.TreeState<typeof ContainerNodeWithDefaults> = [
|
|
814
|
-
{ id: "root", type: "container", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
815
|
-
{ id: "item1", type: "item", parentId: "root", pos: "a0", data: { title: "First", count: 1, active: false } },
|
|
816
|
-
];
|
|
817
|
-
const { env, operations } = createEnvWithDefaultsTree(initialState);
|
|
818
|
-
const proxy = treeWithDefaults._internal.createProxy(env, OperationPath.make(""));
|
|
819
|
-
|
|
820
|
-
// Insert after sibling with only required field
|
|
821
|
-
proxy.insertAfter("item1", ItemNodeWithDefaults, { title: "Second" });
|
|
822
|
-
|
|
823
|
-
const payload = operations[0]!.payload as { data: { title: string; count: number; active: boolean } };
|
|
824
|
-
expect(payload.data.title).toBe("Second");
|
|
825
|
-
expect(payload.data.count).toBe(0); // Default
|
|
826
|
-
expect(payload.data.active).toBe(true); // Default
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
it("insertBefore() uses defaults when fields are omitted", () => {
|
|
830
|
-
const initialState: Primitive.TreeState<typeof ContainerNodeWithDefaults> = [
|
|
831
|
-
{ id: "root", type: "container", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
832
|
-
{ id: "item1", type: "item", parentId: "root", pos: "a0", data: { title: "First", count: 1, active: false } },
|
|
833
|
-
];
|
|
834
|
-
const { env, operations } = createEnvWithDefaultsTree(initialState);
|
|
835
|
-
const proxy = treeWithDefaults._internal.createProxy(env, OperationPath.make(""));
|
|
836
|
-
|
|
837
|
-
// Insert before sibling with only required field, override active
|
|
838
|
-
proxy.insertBefore("item1", ItemNodeWithDefaults, { title: "Zeroth", active: false });
|
|
839
|
-
|
|
840
|
-
const payload = operations[0]!.payload as { data: { title: string; count: number; active: boolean } };
|
|
841
|
-
expect(payload.data.title).toBe("Zeroth");
|
|
842
|
-
expect(payload.data.count).toBe(0); // Default
|
|
843
|
-
expect(payload.data.active).toBe(false); // Overridden
|
|
844
|
-
});
|
|
845
|
-
});
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
describe("TreePrimitive - nested input for set() and default()", () => {
|
|
849
|
-
// Define node types using the new TreeNode API
|
|
850
|
-
// Using TreeNodeSelf for self-referential nodes preserves type safety
|
|
851
|
-
const FileNode = Primitive.TreeNode("file", {
|
|
852
|
-
data: Primitive.Struct({ name: Primitive.String(), size: Primitive.Number().default(0) }),
|
|
853
|
-
children: [] as const,
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
const FolderNode = Primitive.TreeNode("folder", {
|
|
857
|
-
data: Primitive.Struct({ name: Primitive.String() }),
|
|
858
|
-
children: [Primitive.TreeNodeSelf, FileNode],
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
const fileSystemTree = Primitive.Tree({
|
|
862
|
-
root: FolderNode,
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
// Helper to create a mock environment with state access
|
|
866
|
-
const createEnvWithState = (
|
|
867
|
-
state: Primitive.TreeState<typeof FolderNode> = []
|
|
868
|
-
): { env: ReturnType<typeof ProxyEnvironment.make>; operations: Operation.Operation<any, any, any>[] } => {
|
|
869
|
-
const operations: Operation.Operation<any, any, any>[] = [];
|
|
870
|
-
let currentState = [...state] as Primitive.TreeState<typeof FolderNode>;
|
|
871
|
-
let idCounter = 0;
|
|
872
|
-
|
|
873
|
-
const env = ProxyEnvironment.make({
|
|
874
|
-
onOperation: (op) => {
|
|
875
|
-
operations.push(op);
|
|
876
|
-
// Apply operation to keep state in sync
|
|
877
|
-
currentState = fileSystemTree._internal.applyOperation(currentState, op);
|
|
878
|
-
},
|
|
879
|
-
getState: () => currentState,
|
|
880
|
-
generateId: () => `node-${++idCounter}`,
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
return { env, operations };
|
|
884
|
-
};
|
|
885
|
-
|
|
886
|
-
describe("set() with nested input", () => {
|
|
887
|
-
it("converts nested input to flat TreeState", () => {
|
|
888
|
-
const { env, operations } = createEnvWithState();
|
|
889
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
890
|
-
|
|
891
|
-
proxy.set({
|
|
892
|
-
type: "folder",
|
|
893
|
-
name: "Root",
|
|
894
|
-
children: [
|
|
895
|
-
{ type: "file", name: "file1.txt", children: [] },
|
|
896
|
-
{ type: "file", name: "file2.txt", children: [] },
|
|
897
|
-
],
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
expect(operations).toHaveLength(1);
|
|
901
|
-
const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
|
|
902
|
-
expect(payload).toHaveLength(3);
|
|
903
|
-
|
|
904
|
-
// Root
|
|
905
|
-
expect(payload[0]!.type).toBe("folder");
|
|
906
|
-
expect(payload[0]!.parentId).toBe(null);
|
|
907
|
-
expect(payload[0]!.data).toEqual({ name: "Root" });
|
|
908
|
-
|
|
909
|
-
// First file child
|
|
910
|
-
expect(payload[1]!.type).toBe("file");
|
|
911
|
-
expect(payload[1]!.parentId).toBe(payload[0]!.id);
|
|
912
|
-
expect(payload[1]!.data).toEqual({ name: "file1.txt", size: 0 }); // size has default
|
|
913
|
-
|
|
914
|
-
// Second file child
|
|
915
|
-
expect(payload[2]!.type).toBe("file");
|
|
916
|
-
expect(payload[2]!.parentId).toBe(payload[0]!.id);
|
|
917
|
-
expect(payload[2]!.data).toEqual({ name: "file2.txt", size: 0 });
|
|
918
|
-
|
|
919
|
-
// Positions should be in order
|
|
920
|
-
expect(payload[1]!.pos < payload[2]!.pos).toBe(true);
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
it("allows explicit IDs in nested input", () => {
|
|
924
|
-
const { env, operations } = createEnvWithState();
|
|
925
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
926
|
-
|
|
927
|
-
proxy.set({
|
|
928
|
-
type: "folder",
|
|
929
|
-
id: "my-root",
|
|
930
|
-
name: "Root",
|
|
931
|
-
children: [
|
|
932
|
-
{ type: "file", id: "my-file", name: "file.txt", children: [] },
|
|
933
|
-
],
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
|
|
937
|
-
expect(payload[0]!.id).toBe("my-root");
|
|
938
|
-
expect(payload[1]!.id).toBe("my-file");
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
it("throws on duplicate IDs", () => {
|
|
942
|
-
const { env } = createEnvWithState();
|
|
943
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
944
|
-
|
|
945
|
-
expect(() =>
|
|
946
|
-
proxy.set({
|
|
947
|
-
type: "folder",
|
|
948
|
-
id: "dup",
|
|
949
|
-
name: "Root",
|
|
950
|
-
children: [
|
|
951
|
-
{ type: "file", id: "dup", name: "file.txt", children: [] },
|
|
952
|
-
],
|
|
953
|
-
})
|
|
954
|
-
).toThrow(Primitive.ValidationError);
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
it("validates child types against schema", () => {
|
|
958
|
-
const { env } = createEnvWithState();
|
|
959
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
960
|
-
|
|
961
|
-
// File nodes cannot have children with type "folder"
|
|
962
|
-
// But since we can't test FileNode as root (wrong type), let's test folder with wrong child
|
|
963
|
-
// Actually FolderNode accepts both FolderNode and FileNode, so this won't fail on types
|
|
964
|
-
// Let's test wrong root type instead - but the input is typed, so this is caught at compile time
|
|
965
|
-
// We can test runtime by forcing wrong type
|
|
966
|
-
const invalidInput = {
|
|
967
|
-
type: "file" as "folder", // Cast to bypass type check
|
|
968
|
-
name: "WrongRoot",
|
|
969
|
-
children: [],
|
|
970
|
-
};
|
|
971
|
-
|
|
972
|
-
expect(() => proxy.set(invalidInput as any)).toThrow(Primitive.ValidationError);
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
it("handles deeply nested structures", () => {
|
|
976
|
-
const { env, operations } = createEnvWithState();
|
|
977
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
978
|
-
|
|
979
|
-
proxy.set({
|
|
980
|
-
type: "folder",
|
|
981
|
-
name: "Root",
|
|
982
|
-
children: [
|
|
983
|
-
{
|
|
984
|
-
type: "folder" as const,
|
|
985
|
-
name: "Level1",
|
|
986
|
-
children: [
|
|
987
|
-
{
|
|
988
|
-
type: "folder",
|
|
989
|
-
name: "Level2",
|
|
990
|
-
children: [
|
|
991
|
-
{ type: "file", name: "deep.txt", children: [] },
|
|
992
|
-
],
|
|
993
|
-
},
|
|
994
|
-
],
|
|
995
|
-
},
|
|
996
|
-
],
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
|
|
1000
|
-
expect(payload).toHaveLength(4);
|
|
1001
|
-
|
|
1002
|
-
// Verify parent chain (cast data to access name since TreeNodeState.data is unknown)
|
|
1003
|
-
const root = payload.find(n => (n.data as { name: string }).name === "Root");
|
|
1004
|
-
const level1 = payload.find(n => (n.data as { name: string }).name === "Level1");
|
|
1005
|
-
const level2 = payload.find(n => (n.data as { name: string }).name === "Level2");
|
|
1006
|
-
const deep = payload.find(n => (n.data as { name: string }).name === "deep.txt");
|
|
1007
|
-
|
|
1008
|
-
expect(root!.parentId).toBe(null);
|
|
1009
|
-
expect(level1!.parentId).toBe(root!.id);
|
|
1010
|
-
expect(level2!.parentId).toBe(level1!.id);
|
|
1011
|
-
expect(deep!.parentId).toBe(level2!.id);
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
it("applies data defaults to nested input", () => {
|
|
1015
|
-
const { env, operations } = createEnvWithState();
|
|
1016
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
1017
|
-
|
|
1018
|
-
proxy.set({
|
|
1019
|
-
type: "folder",
|
|
1020
|
-
name: "Root",
|
|
1021
|
-
children: [
|
|
1022
|
-
{ type: "file", name: "file.txt", children: [] }, // size omitted, should use default
|
|
1023
|
-
],
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
|
|
1027
|
-
const fileNode = payload.find(n => n.type === "file");
|
|
1028
|
-
expect((fileNode!.data as any).size).toBe(0); // Default value
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
it("prunes optional keys explicitly set to undefined in nested input", () => {
|
|
1032
|
-
const { env, operations } = createEnvWithState();
|
|
1033
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
1034
|
-
|
|
1035
|
-
proxy.set({
|
|
1036
|
-
type: "folder",
|
|
1037
|
-
name: "Root",
|
|
1038
|
-
children: [
|
|
1039
|
-
{ type: "file", name: "file.txt", size: undefined, children: [] },
|
|
1040
|
-
],
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
|
|
1044
|
-
const fileNode = payload.find(n => n.type === "file");
|
|
1045
|
-
expect(fileNode).toBeDefined();
|
|
1046
|
-
expect(fileNode!.data).toEqual({ name: "file.txt" });
|
|
1047
|
-
expect(hasOwn(fileNode!.data, "size")).toBe(false);
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
it("prunes optional keys explicitly set to null in nested input", () => {
|
|
1051
|
-
const { env, operations } = createEnvWithState();
|
|
1052
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
1053
|
-
|
|
1054
|
-
(proxy as any).set({
|
|
1055
|
-
type: "folder",
|
|
1056
|
-
name: "Root",
|
|
1057
|
-
children: [
|
|
1058
|
-
{ type: "file", name: "file.txt", size: null, children: [] },
|
|
1059
|
-
],
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
|
|
1063
|
-
const fileNode = payload.find(n => n.type === "file");
|
|
1064
|
-
expect(fileNode).toBeDefined();
|
|
1065
|
-
expect(fileNode!.data).toEqual({ name: "file.txt" });
|
|
1066
|
-
expect(hasOwn(fileNode!.data, "size")).toBe(false);
|
|
1067
|
-
});
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
describe("default() with nested input", () => {
|
|
1071
|
-
it("creates initial state from nested default", () => {
|
|
1072
|
-
const treeWithDefault = fileSystemTree.default({
|
|
1073
|
-
type: "folder",
|
|
1074
|
-
id: "default-root",
|
|
1075
|
-
name: "Default Root",
|
|
1076
|
-
children: [
|
|
1077
|
-
{ type: "file", id: "default-file", name: "readme.txt", children: [] },
|
|
1078
|
-
],
|
|
1079
|
-
});
|
|
1080
|
-
|
|
1081
|
-
const initialState = treeWithDefault._internal.getInitialState();
|
|
1082
|
-
expect(initialState).toHaveLength(2);
|
|
1083
|
-
|
|
1084
|
-
const root = initialState!.find(n => n.id === "default-root");
|
|
1085
|
-
const file = initialState!.find(n => n.id === "default-file");
|
|
1086
|
-
|
|
1087
|
-
expect(root!.type).toBe("folder");
|
|
1088
|
-
expect(root!.parentId).toBe(null);
|
|
1089
|
-
expect(root!.data).toEqual({ name: "Default Root" });
|
|
1090
|
-
|
|
1091
|
-
expect(file!.type).toBe("file");
|
|
1092
|
-
expect(file!.parentId).toBe("default-root");
|
|
1093
|
-
expect((file!.data as any).size).toBe(0); // Default applied
|
|
1094
|
-
});
|
|
1095
|
-
|
|
1096
|
-
it("generates IDs for default when not provided", () => {
|
|
1097
|
-
const treeWithDefault = fileSystemTree.default({
|
|
1098
|
-
type: "folder",
|
|
1099
|
-
name: "Root",
|
|
1100
|
-
children: [],
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
const initialState = treeWithDefault._internal.getInitialState();
|
|
1104
|
-
expect(initialState).toHaveLength(1);
|
|
1105
|
-
expect(typeof initialState![0]!.id).toBe("string");
|
|
1106
|
-
expect(initialState![0]!.id.length).toBeGreaterThan(0);
|
|
1107
|
-
});
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
describe("sibling ordering", () => {
|
|
1111
|
-
it("maintains children order with correct positions", () => {
|
|
1112
|
-
const { env, operations } = createEnvWithState();
|
|
1113
|
-
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
1114
|
-
|
|
1115
|
-
proxy.set({
|
|
1116
|
-
type: "folder",
|
|
1117
|
-
name: "Root",
|
|
1118
|
-
children: [
|
|
1119
|
-
{ type: "file", id: "first", name: "a.txt", children: [] },
|
|
1120
|
-
{ type: "file", id: "second", name: "b.txt", children: [] },
|
|
1121
|
-
{ type: "file", id: "third", name: "c.txt", children: [] },
|
|
1122
|
-
],
|
|
1123
|
-
});
|
|
1124
|
-
|
|
1125
|
-
const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
|
|
1126
|
-
const first = payload.find(n => n.id === "first")!;
|
|
1127
|
-
const second = payload.find(n => n.id === "second")!;
|
|
1128
|
-
const third = payload.find(n => n.id === "third")!;
|
|
1129
|
-
|
|
1130
|
-
// Positions should be in ascending order
|
|
1131
|
-
expect(first.pos < second.pos).toBe(true);
|
|
1132
|
-
expect(second.pos < third.pos).toBe(true);
|
|
1133
|
-
});
|
|
1134
|
-
});
|
|
1135
|
-
});
|
|
1136
|
-
|
|
1137
|
-
// =============================================================================
|
|
1138
|
-
// Integration Tests - Tree with Complex Structures
|
|
1139
|
-
// =============================================================================
|