dirvi-lib 0.6.1 → 0.7.1
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/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/tree-surfer/cursor.d.ts +9 -0
- package/dist/tree-surfer/cursor.d.ts.map +1 -1
- package/dist/tree-surfer/cursor.js +15 -0
- package/dist/tree-surfer/fold-node/fold-node.schema.d.ts +8 -0
- package/dist/tree-surfer/fold-node/fold-node.schema.d.ts.map +1 -0
- package/dist/tree-surfer/fold-node/fold-node.schema.js +16 -0
- package/dist/tree-surfer/fold-node/fold-node.types.d.ts +7 -7
- package/dist/tree-surfer/fold-node/fold-node.types.d.ts.map +1 -1
- package/dist/tree-surfer/index.d.ts +5 -1
- package/dist/tree-surfer/index.d.ts.map +1 -1
- package/dist/tree-surfer/index.js +5 -1
- package/dist/tree-surfer/state/state.impl.d.ts +7 -0
- package/dist/tree-surfer/state/state.impl.d.ts.map +1 -0
- package/dist/tree-surfer/{state.js → state/state.impl.js} +13 -31
- package/dist/tree-surfer/state/state.schemas.d.ts +9 -0
- package/dist/tree-surfer/state/state.schemas.d.ts.map +1 -0
- package/dist/tree-surfer/state/state.schemas.js +8 -0
- package/dist/tree-surfer/state/state.types.d.ts +17 -0
- package/dist/tree-surfer/state/state.types.d.ts.map +1 -0
- package/dist/tree-surfer/state/state.types.js +1 -0
- package/dist/tree-surfer/tree-node/tree-node.arb.d.ts +82 -13
- package/dist/tree-surfer/tree-node/tree-node.arb.d.ts.map +1 -1
- package/dist/tree-surfer/tree-node/tree-node.arb.debug.d.ts +2 -0
- package/dist/tree-surfer/tree-node/tree-node.arb.debug.d.ts.map +1 -0
- package/dist/tree-surfer/tree-node/tree-node.arb.debug.js +34 -0
- package/dist/tree-surfer/tree-node/tree-node.arb.js +163 -50
- package/dist/tree-surfer/tree-node/tree-node.schemas.d.ts +32 -0
- package/dist/tree-surfer/tree-node/tree-node.schemas.d.ts.map +1 -0
- package/dist/tree-surfer/tree-node/tree-node.schemas.js +35 -0
- package/dist/tree-surfer/tree-node/tree-node.test.js +138 -65
- package/dist/tree-surfer/tree-node/tree-node.types.d.ts +91 -118
- package/dist/tree-surfer/tree-node/tree-node.types.d.ts.map +1 -1
- package/dist/tree-surfer/tree-node/tree-node.types.js +1 -12
- package/dist/tree-surfer-ui/application/intent-to-effect.d.ts +1 -1
- package/dist/tree-surfer-ui/application/intent-to-effect.d.ts.map +1 -1
- package/dist/tree-surfer-ui/application/intent-to-effect.js +9 -13
- package/dist/tree-surfer-ui/application/parse-command.d.ts +4 -0
- package/dist/tree-surfer-ui/application/parse-command.d.ts.map +1 -0
- package/dist/tree-surfer-ui/application/parse-command.js +48 -0
- package/dist/tree-surfer-ui/domain/effect.d.ts +10 -0
- package/dist/tree-surfer-ui/domain/effect.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/domain/posix-node.d.ts +0 -29
- package/dist/domain/posix-node.d.ts.map +0 -1
- package/dist/domain/posix-node.js +0 -20
- package/dist/domain/unix-path.d.ts +0 -6
- package/dist/domain/unix-path.d.ts.map +0 -1
- package/dist/domain/unix-path.js +0 -11
- package/dist/tree-surfer/state.d.ts +0 -19
- package/dist/tree-surfer/state.d.ts.map +0 -1
|
@@ -1,71 +1,172 @@
|
|
|
1
1
|
import * as fc from 'fast-check';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Characters used by readable generated node IDs.
|
|
4
|
+
*/
|
|
5
|
+
const smallLetterCharacters = Array.from('abcdefghijklmnopqrstuvwxyz');
|
|
6
|
+
/**
|
|
7
|
+
* Generates valid string IDs for test nodes. Note that the API just does
|
|
8
|
+
* comparisons on the IDs so we can safely restrict to any subset.
|
|
9
|
+
*/
|
|
10
|
+
export const testIdArb = fc.string({
|
|
11
|
+
unit: fc.constantFrom(...smallLetterCharacters),
|
|
3
12
|
minLength: 1,
|
|
4
13
|
maxLength: 8,
|
|
5
14
|
});
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Generates the metadata for a leaf node.
|
|
17
|
+
*
|
|
18
|
+
* @param id - The ID assigned to the leaf.
|
|
19
|
+
* @returns The leaf and the path that resolves to it.
|
|
20
|
+
*/
|
|
21
|
+
const generatedLeafNodeWithReachableNodes = (id) => {
|
|
22
|
+
const node = { id };
|
|
23
|
+
return {
|
|
24
|
+
node,
|
|
25
|
+
reachableNodes: [{ path: [id], node }],
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Generates the metadata for a closed branch.
|
|
30
|
+
*
|
|
31
|
+
* A closed branch contributes no descendant paths because its children are not
|
|
32
|
+
* loaded.
|
|
33
|
+
*
|
|
34
|
+
* @param id - The ID assigned to the branch.
|
|
35
|
+
* @returns The closed branch and the path that resolves to it.
|
|
36
|
+
*/
|
|
37
|
+
const generatedClosedBranchWithReachableNodes = (id) => {
|
|
38
|
+
const node = {
|
|
39
|
+
id,
|
|
40
|
+
children: null,
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
node,
|
|
44
|
+
reachableNodes: [{ path: [id], node }],
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Generates the metadata for an open branch and its descendants.
|
|
49
|
+
*
|
|
50
|
+
* @param id - The ID assigned to the branch.
|
|
51
|
+
* @param children - The generated children of the branch.
|
|
52
|
+
* @returns The open branch and paths to the branch and all its descendants.
|
|
53
|
+
*/
|
|
54
|
+
const generatedOpenBranchWithReachableNodes = (id, children) => {
|
|
55
|
+
const node = {
|
|
56
|
+
id,
|
|
57
|
+
children: children.map(({ node }) => node),
|
|
58
|
+
};
|
|
59
|
+
const descendantNodes = children.flatMap(({ reachableNodes }) => reachableNodes.map(({ path, node }) => ({
|
|
60
|
+
path: [id, ...path],
|
|
61
|
+
node,
|
|
62
|
+
})));
|
|
63
|
+
return {
|
|
64
|
+
node,
|
|
65
|
+
reachableNodes: [
|
|
66
|
+
{ path: [id], node },
|
|
67
|
+
...descendantNodes,
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Generates a leaf node.
|
|
73
|
+
*/
|
|
74
|
+
export const generatedLeafNodeArb = testIdArb.map(generatedLeafNodeWithReachableNodes);
|
|
75
|
+
/**
|
|
76
|
+
* Generates a closed branch.
|
|
77
|
+
*/
|
|
78
|
+
export const generatedClosedBranchNodeArb = testIdArb.map(generatedClosedBranchWithReachableNodes);
|
|
79
|
+
/**
|
|
80
|
+
* Generates a leaf or a closed branch.
|
|
81
|
+
*
|
|
82
|
+
* These node types terminate a generated path because they have no reachable
|
|
83
|
+
* descendants.
|
|
84
|
+
*/
|
|
85
|
+
const generatedTerminalNodeArb = fc.oneof(generatedLeafNodeArb, generatedClosedBranchNodeArb);
|
|
86
|
+
/**
|
|
87
|
+
* Generates a leaf, an open branch, or a closed branch.
|
|
88
|
+
*
|
|
89
|
+
* Open branches have unique child IDs. Root IDs are made unique separately
|
|
90
|
+
* when generating a forest.
|
|
91
|
+
*
|
|
92
|
+
* The weighted choice gives recursive open branches a geometric dropoff.
|
|
93
|
+
*/
|
|
94
|
+
export const generatedNodeArb = fc.letrec((tie) => {
|
|
95
|
+
const generatedChildNodeArb = tie('node');
|
|
96
|
+
/**
|
|
97
|
+
* Generates the loaded children of an open branch.
|
|
98
|
+
*
|
|
99
|
+
* Sibling IDs must be unique so that child lookup is unambiguous. The
|
|
100
|
+
* maximum length also limits the branching factor of recursive examples.
|
|
101
|
+
*/
|
|
102
|
+
const generatedOpenChildrenArb = fc.uniqueArray(generatedChildNodeArb, {
|
|
20
103
|
minLength: 0,
|
|
104
|
+
maxLength: 4,
|
|
21
105
|
size: 'medium',
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})),
|
|
51
|
-
})).node;
|
|
106
|
+
selector: ({ node }) => node.id,
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* Generates an open branch and its reachable descendants.
|
|
110
|
+
*/
|
|
111
|
+
const generatedOpenBranchNodeArb = fc
|
|
112
|
+
.tuple(testIdArb, generatedOpenChildrenArb)
|
|
113
|
+
.map(([id, children]) => generatedOpenBranchWithReachableNodes(id, children));
|
|
114
|
+
return {
|
|
115
|
+
node: fc
|
|
116
|
+
.integer({
|
|
117
|
+
min: 0,
|
|
118
|
+
max: 9999,
|
|
119
|
+
})
|
|
120
|
+
.chain((choice) =>
|
|
121
|
+
// 5046 works for getAtPath tests, but too heavy for modifyAtPath tests
|
|
122
|
+
choice >= 5050
|
|
123
|
+
? generatedOpenBranchNodeArb
|
|
124
|
+
: generatedTerminalNodeArb),
|
|
125
|
+
};
|
|
126
|
+
}).node;
|
|
127
|
+
/**
|
|
128
|
+
* Generates an array of generated nodes with unique IDs suitable for use as
|
|
129
|
+
* the root of a forest.
|
|
130
|
+
*
|
|
131
|
+
* Root IDs must be unique because paths begin by looking up an ID in this
|
|
132
|
+
* array.
|
|
133
|
+
*/
|
|
52
134
|
export const generatedNodesArrayArb = fc.uniqueArray(generatedNodeArb, {
|
|
53
135
|
minLength: 0,
|
|
54
136
|
size: 'small',
|
|
55
137
|
selector: (generated) => generated.node.id,
|
|
56
138
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
139
|
+
/**
|
|
140
|
+
* An array of root-level nodes and all paths that can be resolved through
|
|
141
|
+
* those nodes.
|
|
142
|
+
*
|
|
143
|
+
* Paths through closed branches stop at the closed branch because its
|
|
144
|
+
* descendants are not loaded.
|
|
145
|
+
*/
|
|
146
|
+
export const nodesArrayAndPathsArb = generatedNodesArrayArb.map((generatedNodes) => ({
|
|
147
|
+
entries: generatedNodes.map(({ node }) => node),
|
|
148
|
+
pathEntries: generatedNodes.flatMap(({ reachableNodes }) => reachableNodes),
|
|
60
149
|
}));
|
|
61
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Generates root-level nodes and a path to one of their reachable nodes.
|
|
152
|
+
*
|
|
153
|
+
* Every generated path is valid for `getAtPath`; unreachable paths are not
|
|
154
|
+
* included.
|
|
155
|
+
*/
|
|
156
|
+
export const nodesArrayAndPathAndExpectedArb = nodesArrayAndPathsArb
|
|
62
157
|
.filter(({ pathEntries }) => pathEntries.length > 0)
|
|
63
158
|
.chain(({ entries, pathEntries }) => fc.constantFrom(...pathEntries).map(({ path, node }) => ({
|
|
64
159
|
entries,
|
|
65
160
|
path,
|
|
66
161
|
expected: node,
|
|
67
162
|
})));
|
|
68
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Generates root-level nodes and a path to one of their branch nodes.
|
|
165
|
+
*
|
|
166
|
+
* Both open and closed branches are included. A path to a closed branch is
|
|
167
|
+
* valid, but a path through a closed branch cannot reach any descendants.
|
|
168
|
+
*/
|
|
169
|
+
export const nodesArrayAndBranchPathArb = nodesArrayAndPathsArb
|
|
69
170
|
.map(({ entries, pathEntries }) => ({
|
|
70
171
|
entries,
|
|
71
172
|
branchPathEntries: pathEntries.filter(({ node }) => 'children' in node),
|
|
@@ -76,5 +177,17 @@ export const nodesArrayAndBranchPathArb = forestAndPathsArb
|
|
|
76
177
|
path,
|
|
77
178
|
expected: node,
|
|
78
179
|
})));
|
|
180
|
+
/**
|
|
181
|
+
* Generates a new array of loaded child nodes.
|
|
182
|
+
*
|
|
183
|
+
* The generated nodes have unique IDs, making the result suitable for use as
|
|
184
|
+
* the children of an open branch.
|
|
185
|
+
*/
|
|
79
186
|
export const newBranchesArb = generatedNodesArrayArb.map((generatedNodes) => generatedNodes.map(({ node }) => node));
|
|
187
|
+
/**
|
|
188
|
+
* Generates either a new array of loaded child nodes or `null`.
|
|
189
|
+
*
|
|
190
|
+
* An array represents an open branch, including an open branch with no
|
|
191
|
+
* children. `null` represents a closed branch.
|
|
192
|
+
*/
|
|
80
193
|
export const newBranchesOrNullArb = fc.oneof(newBranchesArb, fc.constant(null));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SerializableKey, TreeNode } from './tree-node.types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Runtime schema for IDs that can be safely represented as serializable
|
|
5
|
+
* primitive values.
|
|
6
|
+
*
|
|
7
|
+
* Strings are accepted as-is. Numbers must be finite, so `NaN`, `Infinity`,
|
|
8
|
+
* and `-Infinity` are rejected.
|
|
9
|
+
*
|
|
10
|
+
* Applications should derive a more specific schema when those constraints
|
|
11
|
+
* are needed.
|
|
12
|
+
*/
|
|
13
|
+
export declare const serializableKeySchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
14
|
+
/**
|
|
15
|
+
* Helper for creating the schemas for the structural variants of a tree node.
|
|
16
|
+
*
|
|
17
|
+
* The concrete application can extend these schemas with additional fields.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createTreeNodeSchemas<Id extends SerializableKey, ChildNode extends TreeNode<Id, ChildNode>>(idSchema: z.ZodType<Id>, childSchema: z.ZodType<ChildNode>): {
|
|
20
|
+
leafSchema: z.ZodObject<{
|
|
21
|
+
id: z.ZodType<Id, unknown, z.core.$ZodTypeInternals<Id, unknown>>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
closedBranchSchema: z.ZodObject<{
|
|
24
|
+
id: z.ZodType<Id, unknown, z.core.$ZodTypeInternals<Id, unknown>>;
|
|
25
|
+
children: z.ZodNull;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
openBranchSchema: z.ZodObject<{
|
|
28
|
+
id: z.ZodType<Id, unknown, z.core.$ZodTypeInternals<Id, unknown>>;
|
|
29
|
+
children: z.ZodArray<z.ZodType<ChildNode, unknown, z.core.$ZodTypeInternals<ChildNode, unknown>>>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=tree-node.schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-node.schemas.d.ts","sourceRoot":"","sources":["../../../src/tree-surfer/tree-node/tree-node.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEjE;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,iDAA6C,CAAC;AAEhF;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,SAAS,eAAe,EAC1B,SAAS,SAAS,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC,EACzC,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;;;;;;;;;;;;EAoB3D"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Runtime schema for IDs that can be safely represented as serializable
|
|
4
|
+
* primitive values.
|
|
5
|
+
*
|
|
6
|
+
* Strings are accepted as-is. Numbers must be finite, so `NaN`, `Infinity`,
|
|
7
|
+
* and `-Infinity` are rejected.
|
|
8
|
+
*
|
|
9
|
+
* Applications should derive a more specific schema when those constraints
|
|
10
|
+
* are needed.
|
|
11
|
+
*/
|
|
12
|
+
export const serializableKeySchema = z.union([z.string(), z.number().finite()]);
|
|
13
|
+
/**
|
|
14
|
+
* Helper for creating the schemas for the structural variants of a tree node.
|
|
15
|
+
*
|
|
16
|
+
* The concrete application can extend these schemas with additional fields.
|
|
17
|
+
*/
|
|
18
|
+
export function createTreeNodeSchemas(idSchema, childSchema) {
|
|
19
|
+
const leafSchema = z.object({
|
|
20
|
+
id: idSchema,
|
|
21
|
+
});
|
|
22
|
+
const closedBranchSchema = z.object({
|
|
23
|
+
id: idSchema,
|
|
24
|
+
children: z.null(),
|
|
25
|
+
});
|
|
26
|
+
const openBranchSchema = z.object({
|
|
27
|
+
id: idSchema,
|
|
28
|
+
children: z.array(childSchema),
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
leafSchema,
|
|
32
|
+
closedBranchSchema,
|
|
33
|
+
openBranchSchema,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,71 +1,109 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import * as fc from 'fast-check';
|
|
3
3
|
import { createTreeNodeApi } from './tree-node.impl.js';
|
|
4
|
-
import {
|
|
5
|
-
const
|
|
6
|
-
describe('
|
|
7
|
-
it('
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import { newBranchesOrNullArb, nodesArrayAndBranchPathArb, nodesArrayAndPathAndExpectedArb, } from './tree-node.arb.js';
|
|
5
|
+
const api = createTreeNodeApi();
|
|
6
|
+
describe('getChildById', () => {
|
|
7
|
+
it('UT: returns the matching direct child', () => {
|
|
8
|
+
const child = { id: 'child' };
|
|
9
|
+
const root = {
|
|
10
|
+
id: 'root',
|
|
11
|
+
children: [child],
|
|
12
|
+
};
|
|
13
|
+
expect(api.getChildById(root, 'child')).toBe(child);
|
|
14
|
+
});
|
|
15
|
+
it('UT: returns undefined for a missing child', () => {
|
|
16
|
+
const root = {
|
|
17
|
+
id: 'root',
|
|
18
|
+
children: [],
|
|
19
|
+
};
|
|
20
|
+
expect(api.getChildById(root, 'missing')).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe('getAtPath', () => {
|
|
24
|
+
// Using `selector = vi.fn...` here is too heavy
|
|
25
|
+
it('applies selector to the node at a valid path', () => {
|
|
26
|
+
fc.assert(fc.property(nodesArrayAndPathAndExpectedArb, ({ entries, path, expected }) => {
|
|
27
|
+
expect(api.getAtPath(entries, path, (node) => node)).toBe(expected);
|
|
28
|
+
expect(api.getAtPath(entries, path, (node) => node.id)).toBe(expected.id);
|
|
10
29
|
}));
|
|
11
30
|
});
|
|
12
|
-
it('
|
|
31
|
+
it('UT: calls the selector once with the resolved node', () => {
|
|
32
|
+
const expected = { id: 'child' };
|
|
33
|
+
const entries = [
|
|
34
|
+
{
|
|
35
|
+
id: 'root',
|
|
36
|
+
children: [expected],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
const selector = vi.fn((node) => node.id);
|
|
40
|
+
expect(api.getAtPath(entries, ['root', 'child'], selector)).toBe('child');
|
|
41
|
+
expect(selector).toHaveBeenCalledOnce();
|
|
42
|
+
expect(selector).toHaveBeenCalledWith(expected);
|
|
43
|
+
});
|
|
44
|
+
it('UT: returns undefined for an invalid root path without calling the selector', () => {
|
|
45
|
+
const selector = vi.fn((node) => node.id);
|
|
46
|
+
const result = api.getAtPath([{ id: 'root' }], ['missing'], selector);
|
|
47
|
+
expect(result).toBeUndefined();
|
|
48
|
+
expect(selector).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
it('UT: returns undefined when a child is missing from an open branch', () => {
|
|
13
51
|
const entries = [
|
|
14
52
|
{
|
|
15
53
|
id: 'root',
|
|
16
54
|
children: [{ id: 'child' }],
|
|
17
55
|
},
|
|
18
56
|
];
|
|
19
|
-
|
|
57
|
+
const selector = vi.fn((node) => node.id);
|
|
58
|
+
const result = api.getAtPath(entries, ['root', 'missing'], selector);
|
|
59
|
+
expect(result).toBeUndefined();
|
|
60
|
+
expect(selector).not.toHaveBeenCalled();
|
|
20
61
|
});
|
|
21
|
-
it('
|
|
22
|
-
const
|
|
23
|
-
|
|
62
|
+
it('UT: returns undefined for an empty path without calling the selector', () => {
|
|
63
|
+
const selector = vi.fn((node) => node.id);
|
|
64
|
+
const result = api.getAtPath([{ id: 'root' }], [], selector);
|
|
65
|
+
expect(result).toBeUndefined();
|
|
66
|
+
expect(selector).not.toHaveBeenCalled();
|
|
24
67
|
});
|
|
25
|
-
it('
|
|
68
|
+
it('UT: returns undefined through a closed branch without calling the selector', () => {
|
|
26
69
|
const entries = [
|
|
27
70
|
{
|
|
28
71
|
id: 'root',
|
|
29
72
|
children: null,
|
|
30
73
|
},
|
|
31
74
|
];
|
|
32
|
-
|
|
75
|
+
const selector = vi.fn((node) => node.id);
|
|
76
|
+
const result = api.getAtPath(entries, ['root', 'child'], selector);
|
|
77
|
+
expect(result).toBeUndefined();
|
|
78
|
+
expect(selector).not.toHaveBeenCalled();
|
|
33
79
|
});
|
|
34
|
-
it('
|
|
35
|
-
const
|
|
36
|
-
|
|
80
|
+
it('UT: returns undefined when the selector returns undefined', () => {
|
|
81
|
+
const selector = vi.fn((_node) => undefined);
|
|
82
|
+
const result = api.getAtPath([{ id: 'root' }], ['root'], selector);
|
|
83
|
+
expect(result).toBeUndefined();
|
|
84
|
+
expect(selector).toHaveBeenCalledOnce();
|
|
85
|
+
expect(selector).toHaveBeenCalledWith({ id: 'root' });
|
|
37
86
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
children: [child],
|
|
43
|
-
};
|
|
44
|
-
expect(stringNodeApi.isOpenBranch(root)).toBe(true);
|
|
45
|
-
if (!stringNodeApi.isOpenBranch(root)) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
expect(stringNodeApi.getChildById(root, 'child')).toBe(child);
|
|
49
|
-
expect(stringNodeApi.getChildById(root, 'missing')).toBeUndefined();
|
|
50
|
-
});
|
|
51
|
-
it('modifyAtPath replaces the generated node', () => {
|
|
52
|
-
fc.assert(fc.property(forestAndPathAndExpectedArb, ({ entries, path, expected }) => {
|
|
87
|
+
});
|
|
88
|
+
describe('modifyAtPath', () => {
|
|
89
|
+
it('replaces the generated node', () => {
|
|
90
|
+
fc.assert(fc.property(nodesArrayAndPathAndExpectedArb, ({ entries, path, expected }) => {
|
|
53
91
|
const replacement = {
|
|
54
92
|
id: expected.id,
|
|
55
93
|
};
|
|
56
|
-
const updatedForest =
|
|
94
|
+
const updatedForest = api.modifyAtPath(entries, path, () => replacement);
|
|
57
95
|
expect(updatedForest).not.toBeUndefined();
|
|
58
96
|
if (updatedForest === undefined) {
|
|
59
97
|
return;
|
|
60
98
|
}
|
|
61
|
-
expect(
|
|
62
|
-
expect(
|
|
99
|
+
expect(api.getAtPath(updatedForest, path, (node) => node)).toBe(replacement);
|
|
100
|
+
expect(api.getAtPath(entries, path, (node) => node)).toBe(expected);
|
|
63
101
|
}));
|
|
64
|
-
});
|
|
65
|
-
it('
|
|
102
|
+
}, 10000);
|
|
103
|
+
it('replaces children at a branch path', () => {
|
|
66
104
|
fc.assert(fc.property(nodesArrayAndBranchPathArb, newBranchesOrNullArb, ({ entries, path, expected }, newBranches) => {
|
|
67
|
-
expect(
|
|
68
|
-
const updatedForest =
|
|
105
|
+
expect(api.isBranch(expected)).toBe(true);
|
|
106
|
+
const updatedForest = api.modifyAtPath(entries, path, (node) => ({
|
|
69
107
|
...node,
|
|
70
108
|
children: newBranches,
|
|
71
109
|
}));
|
|
@@ -73,45 +111,80 @@ describe('tree node API', () => {
|
|
|
73
111
|
if (updatedForest === undefined) {
|
|
74
112
|
return;
|
|
75
113
|
}
|
|
76
|
-
const updatedNode =
|
|
114
|
+
const updatedNode = api.getAtPath(updatedForest, path, (node) => node);
|
|
77
115
|
expect(updatedNode).not.toBeUndefined();
|
|
78
116
|
expect(updatedNode).not.toBe(expected);
|
|
79
117
|
if (updatedNode === undefined ||
|
|
80
|
-
!
|
|
81
|
-
(!
|
|
118
|
+
!api.isBranch(updatedNode) ||
|
|
119
|
+
(!api.isOpenBranch(updatedNode) && newBranches !== null)) {
|
|
82
120
|
return;
|
|
83
121
|
}
|
|
84
|
-
if (
|
|
85
|
-
expect([...
|
|
122
|
+
if (api.isOpenBranch(updatedNode)) {
|
|
123
|
+
expect([...api.getChildren(updatedNode)]).toStrictEqual(newBranches);
|
|
86
124
|
}
|
|
87
|
-
expect(
|
|
125
|
+
expect(api.getAtPath(entries, path, (node) => node)).toBe(expected);
|
|
88
126
|
}));
|
|
89
127
|
});
|
|
90
|
-
it('
|
|
91
|
-
const
|
|
92
|
-
expect(stringNodeApi.modifyAtPath(entries, ['root'], () => undefined)).toBeUndefined();
|
|
93
|
-
});
|
|
94
|
-
it('modifyAtPath returns undefined for an empty path', () => {
|
|
95
|
-
const entries = [{ id: 'root' }];
|
|
96
|
-
expect(stringNodeApi.modifyAtPath(entries, [], (node) => node)).toBeUndefined();
|
|
97
|
-
});
|
|
98
|
-
it('modifyAtPath returns undefined for an invalid path', () => {
|
|
128
|
+
it('UT: calls the modifier once with the resolved node', () => {
|
|
129
|
+
const child = { id: 'child' };
|
|
99
130
|
const entries = [
|
|
100
131
|
{
|
|
101
132
|
id: 'root',
|
|
102
|
-
children: [],
|
|
133
|
+
children: [child],
|
|
103
134
|
},
|
|
104
135
|
];
|
|
105
|
-
|
|
106
|
-
|
|
136
|
+
const replacement = { id: 'replacement' };
|
|
137
|
+
const modifier = vi.fn(() => replacement);
|
|
138
|
+
const updatedEntries = api.modifyAtPath(entries, ['root', 'child'], modifier);
|
|
139
|
+
expect(updatedEntries).toBeDefined();
|
|
140
|
+
expect(modifier).toHaveBeenCalledOnce();
|
|
141
|
+
expect(modifier).toHaveBeenCalledWith(child);
|
|
142
|
+
expect(api.getAtPath(updatedEntries, ['root', 'replacement'], (node) => node)).toBe(replacement);
|
|
143
|
+
expect(api.getAtPath(updatedEntries, ['root', 'child'], (node) => node)).toBeUndefined();
|
|
107
144
|
});
|
|
108
|
-
it(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
145
|
+
it.each([
|
|
146
|
+
{
|
|
147
|
+
name: 'an empty path',
|
|
148
|
+
entries: [{ id: 'root' }],
|
|
149
|
+
path: [],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'a missing root',
|
|
153
|
+
entries: [{ id: 'root' }],
|
|
154
|
+
path: ['missing'],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'a missing child',
|
|
158
|
+
entries: [
|
|
159
|
+
{
|
|
160
|
+
id: 'root',
|
|
161
|
+
children: [{ id: 'child' }],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
path: ['root', 'missing'],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'a path through a leaf',
|
|
168
|
+
entries: [{ id: 'root' }],
|
|
169
|
+
path: ['root', 'child'],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'a path through a closed branch',
|
|
173
|
+
entries: [{ id: 'root', children: null }],
|
|
174
|
+
path: ['root', 'child'],
|
|
175
|
+
},
|
|
176
|
+
])('UT: returns undefined and does not call the modifier', ({ entries, path }) => {
|
|
177
|
+
const modifier = vi.fn((node) => node);
|
|
178
|
+
const result = api.modifyAtPath(entries, path, modifier);
|
|
179
|
+
expect(result).toBeUndefined();
|
|
180
|
+
expect(modifier).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
it('UT: returns undefined when the modifier aborts', () => {
|
|
183
|
+
const root = [{ id: 'root' }];
|
|
184
|
+
const modifier = vi.fn(() => undefined);
|
|
185
|
+
const result = api.modifyAtPath(root, ['root'], modifier);
|
|
186
|
+
expect(result).toBeUndefined();
|
|
187
|
+
expect(modifier).toHaveBeenCalledOnce();
|
|
188
|
+
expect(modifier).toHaveBeenCalledWith(root[0]);
|
|
116
189
|
});
|
|
117
190
|
});
|