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.
Files changed (53) hide show
  1. package/dist/index.d.ts +0 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +0 -2
  4. package/dist/tree-surfer/cursor.d.ts +9 -0
  5. package/dist/tree-surfer/cursor.d.ts.map +1 -1
  6. package/dist/tree-surfer/cursor.js +15 -0
  7. package/dist/tree-surfer/fold-node/fold-node.schema.d.ts +8 -0
  8. package/dist/tree-surfer/fold-node/fold-node.schema.d.ts.map +1 -0
  9. package/dist/tree-surfer/fold-node/fold-node.schema.js +16 -0
  10. package/dist/tree-surfer/fold-node/fold-node.types.d.ts +7 -7
  11. package/dist/tree-surfer/fold-node/fold-node.types.d.ts.map +1 -1
  12. package/dist/tree-surfer/index.d.ts +5 -1
  13. package/dist/tree-surfer/index.d.ts.map +1 -1
  14. package/dist/tree-surfer/index.js +5 -1
  15. package/dist/tree-surfer/state/state.impl.d.ts +7 -0
  16. package/dist/tree-surfer/state/state.impl.d.ts.map +1 -0
  17. package/dist/tree-surfer/{state.js → state/state.impl.js} +13 -31
  18. package/dist/tree-surfer/state/state.schemas.d.ts +9 -0
  19. package/dist/tree-surfer/state/state.schemas.d.ts.map +1 -0
  20. package/dist/tree-surfer/state/state.schemas.js +8 -0
  21. package/dist/tree-surfer/state/state.types.d.ts +17 -0
  22. package/dist/tree-surfer/state/state.types.d.ts.map +1 -0
  23. package/dist/tree-surfer/state/state.types.js +1 -0
  24. package/dist/tree-surfer/tree-node/tree-node.arb.d.ts +82 -13
  25. package/dist/tree-surfer/tree-node/tree-node.arb.d.ts.map +1 -1
  26. package/dist/tree-surfer/tree-node/tree-node.arb.debug.d.ts +2 -0
  27. package/dist/tree-surfer/tree-node/tree-node.arb.debug.d.ts.map +1 -0
  28. package/dist/tree-surfer/tree-node/tree-node.arb.debug.js +34 -0
  29. package/dist/tree-surfer/tree-node/tree-node.arb.js +163 -50
  30. package/dist/tree-surfer/tree-node/tree-node.schemas.d.ts +32 -0
  31. package/dist/tree-surfer/tree-node/tree-node.schemas.d.ts.map +1 -0
  32. package/dist/tree-surfer/tree-node/tree-node.schemas.js +35 -0
  33. package/dist/tree-surfer/tree-node/tree-node.test.js +138 -65
  34. package/dist/tree-surfer/tree-node/tree-node.types.d.ts +91 -118
  35. package/dist/tree-surfer/tree-node/tree-node.types.d.ts.map +1 -1
  36. package/dist/tree-surfer/tree-node/tree-node.types.js +1 -12
  37. package/dist/tree-surfer-ui/application/intent-to-effect.d.ts +1 -1
  38. package/dist/tree-surfer-ui/application/intent-to-effect.d.ts.map +1 -1
  39. package/dist/tree-surfer-ui/application/intent-to-effect.js +9 -13
  40. package/dist/tree-surfer-ui/application/parse-command.d.ts +4 -0
  41. package/dist/tree-surfer-ui/application/parse-command.d.ts.map +1 -0
  42. package/dist/tree-surfer-ui/application/parse-command.js +48 -0
  43. package/dist/tree-surfer-ui/domain/effect.d.ts +10 -0
  44. package/dist/tree-surfer-ui/domain/effect.d.ts.map +1 -1
  45. package/package.json +3 -2
  46. package/dist/domain/posix-node.d.ts +0 -29
  47. package/dist/domain/posix-node.d.ts.map +0 -1
  48. package/dist/domain/posix-node.js +0 -20
  49. package/dist/domain/unix-path.d.ts +0 -6
  50. package/dist/domain/unix-path.d.ts.map +0 -1
  51. package/dist/domain/unix-path.js +0 -11
  52. package/dist/tree-surfer/state.d.ts +0 -19
  53. package/dist/tree-surfer/state.d.ts.map +0 -1
@@ -1,71 +1,172 @@
1
1
  import * as fc from 'fast-check';
2
- export const idArb = fc.string({
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
- export const generatedNodeArb = fc.letrec((tie) => ({
7
- node: fc.oneof(idArb.map((id) => {
8
- const node = { id: id };
9
- return {
10
- node,
11
- pathEntries: [
12
- {
13
- path: [id],
14
- node,
15
- },
16
- ],
17
- };
18
- }), fc
19
- .tuple(idArb, fc.oneof(fc.constant(null), fc.uniqueArray(tie('node'), {
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
- // @ts-ignore
23
- selector: (generated) => generated.node.id,
24
- })))
25
- .map(([id, children]) => {
26
- const node = {
27
- id,
28
- children:
29
- // @ts-ignore
30
- children === null ? null : children.map((child) => child.node),
31
- };
32
- const descendantPathEntries = children === null
33
- ? []
34
- : children.flatMap((child) =>
35
- // @ts-ignore
36
- child.pathEntries.map(({ path, node }) => ({
37
- path: [id, ...path],
38
- node,
39
- })));
40
- return {
41
- node,
42
- pathEntries: [
43
- {
44
- path: [id],
45
- node,
46
- },
47
- ...descendantPathEntries,
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
- export const forestAndPathsArb = generatedNodesArrayArb.map((generatedNodes) => ({
58
- entries: generatedNodes.map((generated) => generated.node),
59
- pathEntries: generatedNodes.flatMap((generated) => generated.pathEntries),
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
- export const forestAndPathAndExpectedArb = forestAndPathsArb
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
- export const nodesArrayAndBranchPathArb = forestAndPathsArb
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 { forestAndPathAndExpectedArb, newBranchesOrNullArb, nodesArrayAndBranchPathArb, } from './tree-node.arb.js';
5
- const stringNodeApi = createTreeNodeApi();
6
- describe('tree node API', () => {
7
- it('getAtPath returns the node at the generated path', () => {
8
- fc.assert(fc.property(forestAndPathAndExpectedArb, ({ entries, path, expected }) => {
9
- expect(stringNodeApi.getAtPath(entries, path, (node) => node)).toBe(expected);
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('getAtPath applies the selector', () => {
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
- expect(stringNodeApi.getAtPath(entries, ['root', 'child'], (node) => node.id)).toBe('child');
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('getAtPath returns undefined for an empty path', () => {
22
- const entries = [{ id: 'root' }];
23
- expect(stringNodeApi.getAtPath(entries, [], (node) => node)).toBeUndefined();
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('getAtPath returns undefined through a closed branch', () => {
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
- expect(stringNodeApi.getAtPath(entries, ['root', 'child'], (node) => node)).toBeUndefined();
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('getAtPath returns undefined when the selector returns undefined', () => {
35
- const entries = [{ id: 'root' }];
36
- expect(stringNodeApi.getAtPath(entries, ['root'], () => undefined)).toBeUndefined();
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
- it('getChildById returns the matching child', () => {
39
- const child = { id: 'child' };
40
- const root = {
41
- id: 'root',
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 = stringNodeApi.modifyAtPath(entries, path, () => replacement);
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(stringNodeApi.getAtPath(updatedForest, path, (node) => node)).toBe(replacement);
62
- expect(stringNodeApi.getAtPath(entries, path, (node) => node)).toBe(expected);
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('modifyAtPath replaces children at a branch path', () => {
102
+ }, 10000);
103
+ it('replaces children at a branch path', () => {
66
104
  fc.assert(fc.property(nodesArrayAndBranchPathArb, newBranchesOrNullArb, ({ entries, path, expected }, newBranches) => {
67
- expect(stringNodeApi.isBranch(expected)).toBe(true);
68
- const updatedForest = stringNodeApi.modifyAtPath(entries, path, (node) => ({
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 = stringNodeApi.getAtPath(updatedForest, path, (node) => node);
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
- !stringNodeApi.isBranch(updatedNode) ||
81
- (!stringNodeApi.isOpenBranch(updatedNode) && newBranches !== null)) {
118
+ !api.isBranch(updatedNode) ||
119
+ (!api.isOpenBranch(updatedNode) && newBranches !== null)) {
82
120
  return;
83
121
  }
84
- if (stringNodeApi.isOpenBranch(updatedNode)) {
85
- expect([...stringNodeApi.getChildren(updatedNode)]).toStrictEqual(newBranches);
122
+ if (api.isOpenBranch(updatedNode)) {
123
+ expect([...api.getChildren(updatedNode)]).toStrictEqual(newBranches);
86
124
  }
87
- expect(stringNodeApi.getAtPath(entries, path, (node) => node)).toBe(expected);
125
+ expect(api.getAtPath(entries, path, (node) => node)).toBe(expected);
88
126
  }));
89
127
  });
90
- it('modifyAtPath returns undefined when the modifier aborts', () => {
91
- const entries = [{ id: 'root' }];
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
- expect(stringNodeApi.modifyAtPath(entries, ['missing'], (node) => node)).toBeUndefined();
106
- expect(stringNodeApi.modifyAtPath(entries, ['root', 'missing'], (node) => node)).toBeUndefined();
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('modifyAtPath cannot traverse a closed branch', () => {
109
- const entries = [
110
- {
111
- id: 'root',
112
- children: null,
113
- },
114
- ];
115
- expect(stringNodeApi.modifyAtPath(entries, ['root', 'child'], (node) => node)).toBeUndefined();
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
  });