@weborigami/async-tree 0.0.60 → 0.0.62

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.0.60",
3
+ "version": "0.0.62",
4
4
  "description": "Asynchronous tree drivers based on standard JavaScript classes",
5
5
  "type": "module",
6
6
  "main": "./main.js",
@@ -11,7 +11,7 @@
11
11
  "typescript": "5.5.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/types": "0.0.60"
14
+ "@weborigami/types": "0.0.62"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --test --test-reporter=spec",
@@ -1,5 +1,10 @@
1
1
  import { Tree } from "./internal.js";
2
- import { hiddenFileNames, isStringLike, naturalOrder } from "./utilities.js";
2
+ import {
3
+ hiddenFileNames,
4
+ isStringLike,
5
+ naturalOrder,
6
+ setParent,
7
+ } from "./utilities.js";
3
8
 
4
9
  const TypedArray = Object.getPrototypeOf(Uint8Array);
5
10
 
@@ -34,7 +39,9 @@ export default class BrowserFileTree {
34
39
  // Try the key as a subfolder name.
35
40
  try {
36
41
  const subfolderHandle = await directory.getDirectoryHandle(key);
37
- return Reflect.construct(this.constructor, [subfolderHandle]);
42
+ const value = Reflect.construct(this.constructor, [subfolderHandle]);
43
+ setParent(value, this);
44
+ return value;
38
45
  } catch (error) {
39
46
  if (
40
47
  !(
@@ -50,7 +57,9 @@ export default class BrowserFileTree {
50
57
  try {
51
58
  const fileHandle = await directory.getFileHandle(key);
52
59
  const file = await fileHandle.getFile();
53
- return file.arrayBuffer();
60
+ const buffer = file.arrayBuffer();
61
+ setParent(buffer, this);
62
+ return buffer;
54
63
  } catch (error) {
55
64
  if (!(error instanceof DOMException && error.name === "NotFoundError")) {
56
65
  throw error;
@@ -12,15 +12,11 @@ export default class DeepObjectTree extends ObjectTree {
12
12
  value = Reflect.construct(this.constructor, [value]);
13
13
  }
14
14
 
15
- if (Tree.isAsyncTree(value) && !value.parent) {
16
- value.parent = this;
17
- }
18
-
19
15
  return value;
20
16
  }
21
17
 
22
18
  async isKeyForSubtree(key) {
23
- const value = this.object[key];
19
+ const value = await this.object[key];
24
20
  return isPlainObject(value) || Tree.isAsyncTree(value);
25
21
  }
26
22
  }
package/src/FileTree.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  isPacked,
9
9
  isPlainObject,
10
10
  naturalOrder,
11
+ setParent,
11
12
  } from "./utilities.js";
12
13
 
13
14
  /**
@@ -65,16 +66,18 @@ export default class FileTree {
65
66
  throw error;
66
67
  }
67
68
 
69
+ let value;
68
70
  if (stats.isDirectory()) {
69
71
  // Return subdirectory as a tree
70
- const subtree = Reflect.construct(this.constructor, [filePath]);
71
- subtree.parent = this;
72
- return subtree;
72
+ value = Reflect.construct(this.constructor, [filePath]);
73
73
  } else {
74
74
  // Return file contents as a standard Uint8Array.
75
75
  const buffer = await fs.readFile(filePath);
76
- return Uint8Array.from(buffer);
76
+ value = Uint8Array.from(buffer);
77
77
  }
78
+
79
+ setParent(value, this);
80
+ return value;
78
81
  }
79
82
 
80
83
  async isKeyForSubtree(key) {
@@ -1,3 +1,5 @@
1
+ import { setParent } from "./utilities.js";
2
+
1
3
  /**
2
4
  * A tree defined by a function and an optional domain.
3
5
  *
@@ -30,7 +32,7 @@ export default class FunctionTree {
30
32
  // eventually bind all parameters until only one remains. At that point,
31
33
  // the above condition will apply and the function will be invoked.
32
34
  Reflect.construct(this.constructor, [this.fn.bind(null, key)]);
33
-
35
+ setParent(value, this);
34
36
  return value;
35
37
  }
36
38
 
package/src/MapTree.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "./internal.js";
2
+ import { setParent } from "./utilities.js";
2
3
 
3
4
  /**
4
5
  * A tree backed by a JavaScript `Map` object.
@@ -22,12 +23,8 @@ export default class MapTree {
22
23
  }
23
24
 
24
25
  async get(key) {
25
- let value = this.map.get(key);
26
-
27
- if (Tree.isAsyncTree(value) && !value.parent) {
28
- value.parent = this;
29
- }
30
-
26
+ const value = this.map.get(key);
27
+ setParent(value, this);
31
28
  return value;
32
29
  }
33
30
 
package/src/ObjectTree.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Tree } from "./internal.js";
2
- import { getRealmObjectPrototype } from "./utilities.js";
2
+ import * as symbols from "./symbols.js";
3
+ import { getRealmObjectPrototype, setParent } from "./utilities.js";
3
4
 
4
5
  /**
5
6
  * A tree defined by a plain object or array.
@@ -15,7 +16,7 @@ export default class ObjectTree {
15
16
  */
16
17
  constructor(object) {
17
18
  this.object = object;
18
- this.parent = null;
19
+ this.parent = object[symbols.parent] ?? null;
19
20
  }
20
21
 
21
22
  /**
@@ -35,11 +36,8 @@ export default class ObjectTree {
35
36
  return undefined;
36
37
  }
37
38
 
38
- let value = this.object[key];
39
-
40
- if (Tree.isAsyncTree(value) && !value.parent) {
41
- value.parent = this;
42
- }
39
+ let value = await this.object[key];
40
+ setParent(value, this);
43
41
 
44
42
  if (typeof value === "function" && !Object.hasOwn(this.object, key)) {
45
43
  // Value is an inherited method; bind it to the object.
package/src/SetTree.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "./internal.js";
2
+ import { setParent } from "./utilities.js";
2
3
 
3
4
  /**
4
5
  * A tree of Set objects.
@@ -16,12 +17,8 @@ export default class SetTree {
16
17
  }
17
18
 
18
19
  async get(key) {
19
- let value = this.values[key];
20
-
21
- if (Tree.isAsyncTree(value) && !value.parent) {
22
- value.parent = this;
23
- }
24
-
20
+ const value = this.values[key];
21
+ setParent(value, this);
25
22
  return value;
26
23
  }
27
24
 
package/src/SiteTree.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Tree } from "./internal.js";
2
2
  import * as keysJson from "./keysJson.js";
3
+ import { setParent } from "./utilities.js";
3
4
 
4
5
  /**
5
6
  * A tree of values obtained via HTTP/HTTPS calls. These values will be strings
@@ -44,7 +45,9 @@ export default class SiteTree {
44
45
  // If the (possibly adjusted) route ends with a slash and the site is an
45
46
  // explorable site, we return a tree for the indicated route.
46
47
  if (href.endsWith("/") && (await this.hasKeysJson())) {
47
- return Reflect.construct(this.constructor, [href]);
48
+ const value = Reflect.construct(this.constructor, [href]);
49
+ setParent(value, this);
50
+ return value;
48
51
  }
49
52
 
50
53
  // Fetch the data at the given route.
@@ -67,9 +70,13 @@ export default class SiteTree {
67
70
  }
68
71
 
69
72
  const mediaType = response.headers?.get("Content-Type");
70
- return SiteTree.mediaTypeIsText(mediaType)
71
- ? response.text()
72
- : response.arrayBuffer();
73
+ if (SiteTree.mediaTypeIsText(mediaType)) {
74
+ return response.text();
75
+ } else {
76
+ const buffer = response.arrayBuffer();
77
+ setParent(buffer, this);
78
+ return buffer;
79
+ }
73
80
  }
74
81
 
75
82
  async getKeyDictionary() {
package/src/Tree.d.ts CHANGED
@@ -5,7 +5,7 @@ export function assign(target: Treelike, source: Treelike): Promise<AsyncTree>;
5
5
  export function clear(AsyncTree: AsyncMutableTree): Promise<void>;
6
6
  export function entries(AsyncTree: AsyncTree): Promise<IterableIterator<any>>;
7
7
  export function forEach(AsyncTree: AsyncTree, callbackfn: (value: any, key: any) => Promise<void>): Promise<void>;
8
- export function from(obj: any): AsyncTree;
8
+ export function from(obj: any, options?: { deep: boolean }): AsyncTree;
9
9
  export function has(AsyncTree: AsyncTree, key: any): Promise<boolean>;
10
10
  export function isAsyncMutableTree(obj: any): obj is AsyncMutableTree;
11
11
  export function isAsyncTree(obj: any): obj is AsyncTree;
package/src/Tree.js CHANGED
@@ -103,32 +103,37 @@ export async function forEach(tree, callbackFn) {
103
103
  /**
104
104
  * Attempts to cast the indicated object to an async tree.
105
105
  *
106
- * @param {Treelike | Object} obj
106
+ * If the object is a plain object, it will be converted to an ObjectTree. The
107
+ * optional `deep` option can be set to `true` to convert a plain object to a
108
+ * DeepObjectTree.
109
+ *
110
+ * @param {Treelike | Object} object
111
+ * @param {{ deep?: true }} [options]
107
112
  * @returns {AsyncTree}
108
113
  */
109
- export function from(obj) {
110
- if (isAsyncTree(obj)) {
114
+ export function from(object, options = {}) {
115
+ if (isAsyncTree(object)) {
111
116
  // Argument already supports the tree interface.
112
117
  // @ts-ignore
113
- return obj;
114
- } else if (typeof obj === "function") {
115
- return new FunctionTree(obj);
116
- } else if (obj instanceof Map) {
117
- return new MapTree(obj);
118
- } else if (obj instanceof Set) {
119
- return new SetTree(obj);
120
- } else if (isPlainObject(obj)) {
121
- return new DeepObjectTree(obj);
122
- } else if (isUnpackable(obj)) {
118
+ return object;
119
+ } else if (typeof object === "function") {
120
+ return new FunctionTree(object);
121
+ } else if (object instanceof Map) {
122
+ return new MapTree(object);
123
+ } else if (object instanceof Set) {
124
+ return new SetTree(object);
125
+ } else if (isPlainObject(object) || object instanceof Array) {
126
+ return options.deep ? new DeepObjectTree(object) : new ObjectTree(object);
127
+ } else if (isUnpackable(object)) {
123
128
  async function AsyncFunction() {} // Sample async function
124
- return obj.unpack instanceof AsyncFunction.constructor
129
+ return object.unpack instanceof AsyncFunction.constructor
125
130
  ? // Async unpack: return a deferred tree.
126
- new DeferredTree(obj.unpack)
131
+ new DeferredTree(object.unpack)
127
132
  : // Synchronous unpack: cast the result of unpack() to a tree.
128
- from(obj.unpack());
129
- } else if (obj && typeof obj === "object") {
133
+ from(object.unpack());
134
+ } else if (object && typeof object === "object") {
130
135
  // An instance of some class.
131
- return new ObjectTree(obj);
136
+ return new ObjectTree(object);
132
137
  }
133
138
 
134
139
  throw new TypeError("Couldn't convert argument to an async tree");
@@ -8,7 +8,7 @@ import { Tree } from "../internal.js";
8
8
  * @returns {AsyncTree & { description: string }}
9
9
  */
10
10
  export default function deepMerge(...sources) {
11
- let trees = sources.map((treelike) => Tree.from(treelike));
11
+ let trees = sources.map((treelike) => Tree.from(treelike, { deep: true }));
12
12
  let mergeParent;
13
13
  return {
14
14
  description: "deepMerge",
@@ -11,7 +11,7 @@ export default function deepTakeFn(count) {
11
11
  * @param {import("../../index.ts").Treelike} treelike
12
12
  */
13
13
  return async function deepTakeFn(treelike) {
14
- const tree = await Tree.from(treelike);
14
+ const tree = await Tree.from(treelike, { deep: true });
15
15
  const { values } = await traverse(tree, count);
16
16
  return Tree.from(values);
17
17
  };
@@ -14,7 +14,7 @@ export default async function* deepValuesIterator(
14
14
  treelike,
15
15
  options = { expand: false }
16
16
  ) {
17
- const tree = Tree.from(treelike);
17
+ const tree = Tree.from(treelike, { deep: true });
18
18
  for (const key of await tree.keys()) {
19
19
  let value = await tree.get(key);
20
20
 
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import * as symbols from "../symbols.js";
2
3
 
3
4
  /**
4
5
  * Return a tree that performs a shallow merge of the given trees.
@@ -14,18 +15,24 @@ import { Tree } from "../internal.js";
14
15
  * @returns {AsyncTree & { description: string, trees: AsyncTree[]}}
15
16
  */
16
17
  export default function merge(...sources) {
18
+ const trees = sources.map((treelike) => Tree.from(treelike));
17
19
  return {
18
20
  description: "merge",
19
21
 
20
22
  async get(key) {
21
23
  // Check trees for the indicated key in reverse order.
22
- for (let index = this.trees.length - 1; index >= 0; index--) {
23
- const tree = this.trees[index];
24
+ for (let index = trees.length - 1; index >= 0; index--) {
25
+ const tree = trees[index];
24
26
  const value = await tree.get(key);
25
27
  if (value !== undefined) {
28
+ // Merged tree acts as parent instead of the source tree.
26
29
  if (Tree.isAsyncTree(value) && value.parent === tree) {
27
- // Merged tree acts as parent instead of the source tree.
28
30
  value.parent = this;
31
+ } else if (
32
+ typeof value === "object" &&
33
+ value?.[symbols.parent] === tree
34
+ ) {
35
+ value[symbols.parent] = this;
29
36
  }
30
37
  return value;
31
38
  }
@@ -35,8 +42,8 @@ export default function merge(...sources) {
35
42
 
36
43
  async isKeyForSubtree(key) {
37
44
  // Check trees for the indicated key in reverse order.
38
- for (let index = this.trees.length - 1; index >= 0; index--) {
39
- if (await Tree.isKeyForSubtree(this.trees[index], key)) {
45
+ for (let index = trees.length - 1; index >= 0; index--) {
46
+ if (await Tree.isKeyForSubtree(trees[index], key)) {
40
47
  return true;
41
48
  }
42
49
  }
@@ -46,7 +53,7 @@ export default function merge(...sources) {
46
53
  async keys() {
47
54
  const keys = new Set();
48
55
  // Collect keys in the order the trees were provided.
49
- for (const tree of this.trees) {
56
+ for (const tree of trees) {
50
57
  for (const key of await tree.keys()) {
51
58
  keys.add(key);
52
59
  }
@@ -54,6 +61,8 @@ export default function merge(...sources) {
54
61
  return keys;
55
62
  },
56
63
 
57
- trees: sources.map((treelike) => Tree.from(treelike)),
64
+ get trees() {
65
+ return trees;
66
+ },
58
67
  };
59
68
  }
@@ -1,5 +1,4 @@
1
- import { ObjectTree, Tree } from "../internal.js";
2
- import { isPlainObject } from "../utilities.js";
1
+ import { Tree } from "../internal.js";
3
2
 
4
3
  /**
5
4
  * Return a transform function that maps the keys and/or values of a tree.
@@ -47,10 +46,7 @@ export default function createMapTransform(options = {}) {
47
46
  * @type {import("../../index.ts").TreeTransform}
48
47
  */
49
48
  return function map(treelike) {
50
- const tree =
51
- !deep && isPlainObject(treelike) && !Tree.isAsyncTree(treelike)
52
- ? new ObjectTree(treelike)
53
- : Tree.from(treelike);
49
+ const tree = Tree.from(treelike, { deep });
54
50
 
55
51
  // The transformed tree is actually an extension of the original tree's
56
52
  // prototype chain. This allows the transformed tree to inherit any
@@ -1,5 +1,7 @@
1
+ import { AsyncTree } from "@weborigami/types";
1
2
  import { Packed, PlainObject, StringLike } from "../index.ts";
2
3
 
4
+ export function box(value: any): any;
3
5
  export function castArrayLike(object: any): any;
4
6
  export function getRealmObjectPrototype(object: any): any;
5
7
  export const hiddenFileNames: string[];
@@ -10,5 +12,6 @@ export function isStringLike(object: any): object is StringLike;
10
12
  export function keysFromPath(path: string): string[];
11
13
  export const naturalOrder: (a: string, b: string) => number;
12
14
  export function pipeline(start: any, ...functions: Function[]): Promise<any>;
15
+ export function setParent(child: any, parent: AsyncTree): void;
13
16
  export function toPlainValue(object: any): Promise<any>;
14
- export function toString(object: any): string;
17
+ export function toString(object: any): string;
package/src/utilities.js CHANGED
@@ -1,8 +1,30 @@
1
1
  import { Tree } from "./internal.js";
2
+ import * as symbols from "./symbols.js";
2
3
 
3
4
  const textDecoder = new TextDecoder();
4
5
  const TypedArray = Object.getPrototypeOf(Uint8Array);
5
6
 
7
+ /**
8
+ * Return the value as an object. If the value is already an object it will be
9
+ * returned as is. If the value is a primitive, it will be wrapped in an object:
10
+ * a string will be wrapped in a String object, a number will be wrapped in a
11
+ * Number object, and a boolean will be wrapped in a Boolean object.
12
+ *
13
+ * @param {any} value
14
+ */
15
+ export function box(value) {
16
+ switch (typeof value) {
17
+ case "string":
18
+ return new String(value);
19
+ case "number":
20
+ return new Number(value);
21
+ case "boolean":
22
+ return new Boolean(value);
23
+ default:
24
+ return value;
25
+ }
26
+ }
27
+
6
28
  /**
7
29
  * If the given plain object has only sequential integer keys, return it as an
8
30
  * array. Otherwise return it as is.
@@ -170,6 +192,35 @@ export async function pipeline(start, ...fns) {
170
192
  return fns.reduce(async (acc, fn) => fn(await acc), start);
171
193
  }
172
194
 
195
+ /**
196
+ * If the child object doesn't have a parent yet, set it to the indicated
197
+ * parent. If the child is an AsyncTree, set the `parent` property. Otherwise,
198
+ * set the `symbols.parent` property.
199
+ *
200
+ * @param {*} child
201
+ * @param {*} parent
202
+ */
203
+ export function setParent(child, parent) {
204
+ if (Tree.isAsyncTree(child)) {
205
+ // Value is a subtree; set its parent to this tree.
206
+ if (!child.parent) {
207
+ child.parent = parent;
208
+ }
209
+ } else if (Object.isExtensible(child) && !child[symbols.parent]) {
210
+ // Add parent reference as a symbol to avoid polluting the object. This
211
+ // reference will be used if the object is later used as a tree. We set
212
+ // `enumerable` to false even thought this makes no practical difference
213
+ // (symbols are never enumerated) because it can provide a hint in the
214
+ // debugger that the property is for internal use.
215
+ Object.defineProperty(child, symbols.parent, {
216
+ configurable: true,
217
+ enumerable: false,
218
+ value: parent,
219
+ writable: true,
220
+ });
221
+ }
222
+ }
223
+
173
224
  /**
174
225
  * Convert the given input to the plainest possible JavaScript value. This
175
226
  * helper is intended for functions that want to accept an argument from the ori
@@ -1,6 +1,7 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import { ObjectTree, Tree } from "../src/internal.js";
4
+ import * as symbols from "../src/symbols.js";
4
5
 
5
6
  describe("ObjectTree", () => {
6
7
  test("can get the keys of the tree", async () => {
@@ -76,6 +77,14 @@ describe("ObjectTree", () => {
76
77
  assert.equal(await fixture.get("prop"), "Goodbye");
77
78
  });
78
79
 
80
+ test("sets parent symbol on subobjects", async () => {
81
+ const fixture = new ObjectTree({
82
+ sub: {},
83
+ });
84
+ const sub = await fixture.get("sub");
85
+ assert.equal(sub[symbols.parent], fixture);
86
+ });
87
+
79
88
  test("sets parent on subtrees", async () => {
80
89
  const fixture = new ObjectTree({
81
90
  a: 1,
package/test/Tree.test.js CHANGED
@@ -97,6 +97,16 @@ describe("Tree", () => {
97
97
  });
98
98
  });
99
99
 
100
+ test("from returns a deep object tree if deep option is true", async () => {
101
+ const obj = {
102
+ sub: {
103
+ a: 1,
104
+ },
105
+ };
106
+ const tree = Tree.from(obj, { deep: true });
107
+ assert(tree instanceof DeepObjectTree);
108
+ });
109
+
100
110
  test("from() creates a deferred tree if unpack() returns a promise", async () => {
101
111
  const obj = new String();
102
112
  /** @type {any} */ (obj).unpack = async () => ({
@@ -232,6 +242,15 @@ describe("Tree", () => {
232
242
  assert.deepEqual(plain, original);
233
243
  });
234
244
 
245
+ test("plain() awaits async properties", async () => {
246
+ const object = {
247
+ get name() {
248
+ return Promise.resolve("Alice");
249
+ },
250
+ };
251
+ assert.deepEqual(await Tree.plain(object), { name: "Alice" });
252
+ });
253
+
235
254
  test("remove method removes a value", async () => {
236
255
  const fixture = createFixture();
237
256
  await Tree.remove(fixture, "Alice.md");
@@ -19,4 +19,10 @@ describe("deepValues", () => {
19
19
  const values = await deepValues(tree);
20
20
  assert.deepEqual(values, [1, 2, 3, 4]);
21
21
  });
22
+
23
+ test("returns in-order array of values in nested arrays", async () => {
24
+ const tree = [1, [2, 3], 4];
25
+ const values = await deepValues(tree);
26
+ assert.deepEqual(values, [1, 2, 3, 4]);
27
+ });
22
28
  });
@@ -2,6 +2,7 @@ import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import { Tree } from "../../src/internal.js";
4
4
  import merge from "../../src/operations/merge.js";
5
+ import * as symbols from "../../src/symbols.js";
5
6
 
6
7
  describe("merge", () => {
7
8
  test("performs a shallow merge", async () => {
@@ -38,7 +39,7 @@ describe("merge", () => {
38
39
  assert.equal(c, undefined);
39
40
 
40
41
  // Parent of a subvalue is the merged tree
41
- const b = await Tree.traverse(fixture, "b");
42
- assert.equal(b.parent, fixture);
42
+ const b = await fixture.get("b");
43
+ assert.equal(b[symbols.parent], fixture);
43
44
  });
44
45
  });
@@ -1,8 +1,31 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
+ import { symbols } from "../main.js";
4
+ import { ObjectTree } from "../src/internal.js";
3
5
  import * as utilities from "../src/utilities.js";
4
6
 
5
7
  describe("utilities", () => {
8
+ test("box returns a boxed value", () => {
9
+ const string = "string";
10
+ const stringObject = utilities.box(string);
11
+ assert(stringObject instanceof String);
12
+ assert.equal(stringObject, string);
13
+
14
+ const number = 1;
15
+ const numberObject = utilities.box(number);
16
+ assert(numberObject instanceof Number);
17
+ assert.equal(numberObject, number);
18
+
19
+ const boolean = true;
20
+ const booleanObject = utilities.box(boolean);
21
+ assert(booleanObject instanceof Boolean);
22
+ assert.equal(booleanObject, boolean);
23
+
24
+ const object = {};
25
+ const boxedObject = utilities.box(object);
26
+ assert.equal(boxedObject, object);
27
+ });
28
+
6
29
  test("getRealmObjectPrototype returns the object's root prototype", () => {
7
30
  const object = {};
8
31
  const proto = utilities.getRealmObjectPrototype(object);
@@ -36,6 +59,33 @@ describe("utilities", () => {
36
59
  assert.equal(result, 16);
37
60
  });
38
61
 
62
+ test("setParent sets a child's parent", () => {
63
+ const parent = new ObjectTree({});
64
+
65
+ // Set [symbols.parent] on a plain object.
66
+ const object = {};
67
+ utilities.setParent(object, parent);
68
+ assert.equal(object[symbols.parent], parent);
69
+
70
+ // Leave [symbols.parent] alone if it's already set.
71
+ const childWithParent = {
72
+ [symbols.parent]: "parent",
73
+ };
74
+ utilities.setParent(childWithParent, parent);
75
+ assert.equal(childWithParent[symbols.parent], "parent");
76
+
77
+ // Set `parent` on a tree.
78
+ const tree = new ObjectTree({});
79
+ utilities.setParent(tree, parent);
80
+ assert.equal(tree.parent, parent);
81
+
82
+ // Leave `parent` alone if it's already set.
83
+ const treeWithParent = new ObjectTree({});
84
+ treeWithParent.parent = "parent";
85
+ utilities.setParent(treeWithParent, parent);
86
+ assert.equal(treeWithParent.parent, "parent");
87
+ });
88
+
39
89
  test("toPlainValue returns the plainest representation of an object", async () => {
40
90
  class User {
41
91
  constructor(name) {