@weborigami/async-tree 0.0.35

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 (57) hide show
  1. package/ReadMe.md +1 -0
  2. package/browser.js +13 -0
  3. package/index.ts +44 -0
  4. package/main.js +23 -0
  5. package/package.json +20 -0
  6. package/src/BrowserFileTree.js +137 -0
  7. package/src/DeferredTree.js +72 -0
  8. package/src/FileTree.js +193 -0
  9. package/src/FunctionTree.js +47 -0
  10. package/src/MapTree.js +51 -0
  11. package/src/ObjectTree.js +103 -0
  12. package/src/SetTree.js +40 -0
  13. package/src/SiteTree.js +124 -0
  14. package/src/Tree.d.ts +22 -0
  15. package/src/Tree.js +404 -0
  16. package/src/keysJson.d.ts +4 -0
  17. package/src/keysJson.js +56 -0
  18. package/src/operations/cache.js +75 -0
  19. package/src/operations/merge.js +60 -0
  20. package/src/operations/mergeDeep.js +47 -0
  21. package/src/transforms/cachedKeyMaps.js +86 -0
  22. package/src/transforms/groupBy.js +40 -0
  23. package/src/transforms/keyMapsForExtensions.js +64 -0
  24. package/src/transforms/map.js +106 -0
  25. package/src/transforms/regExpKeys.js +65 -0
  26. package/src/transforms/sort.js +22 -0
  27. package/src/transforms/sortBy.js +29 -0
  28. package/src/transforms/sortNatural.js +10 -0
  29. package/src/utilities.d.ts +10 -0
  30. package/src/utilities.js +124 -0
  31. package/test/BrowserFileTree.test.js +119 -0
  32. package/test/DeferredTree.test.js +23 -0
  33. package/test/FileTree.test.js +134 -0
  34. package/test/FunctionTree.test.js +51 -0
  35. package/test/MapTree.test.js +37 -0
  36. package/test/ObjectTree.test.js +159 -0
  37. package/test/SetTree.test.js +32 -0
  38. package/test/SiteTree.test.js +110 -0
  39. package/test/Tree.test.js +347 -0
  40. package/test/browser/assert.js +45 -0
  41. package/test/browser/index.html +35 -0
  42. package/test/browser/testRunner.js +51 -0
  43. package/test/fixtures/markdown/Alice.md +1 -0
  44. package/test/fixtures/markdown/Bob.md +1 -0
  45. package/test/fixtures/markdown/Carol.md +1 -0
  46. package/test/operations/cache.test.js +57 -0
  47. package/test/operations/merge.test.js +39 -0
  48. package/test/operations/mergeDeep.test.js +38 -0
  49. package/test/transforms/cachedKeyMaps.test.js +41 -0
  50. package/test/transforms/groupBy.test.js +29 -0
  51. package/test/transforms/keyMapsForExtensions.test.js +70 -0
  52. package/test/transforms/map.test.js +174 -0
  53. package/test/transforms/regExpKeys.test.js +25 -0
  54. package/test/transforms/sort.test.js +30 -0
  55. package/test/transforms/sortBy.test.js +22 -0
  56. package/test/transforms/sortNatural.test.js +21 -0
  57. package/test/utilities.test.js +24 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * If the given plain object has only sequential integer keys, return it as an
3
+ * array. Otherwise return it as is.
4
+ *
5
+ * @param {any} object
6
+ */
7
+ export function castArrayLike(object) {
8
+ let hasKeys = false;
9
+ let expectedIndex = 0;
10
+ for (const key in object) {
11
+ hasKeys = true;
12
+ const index = Number(key);
13
+ if (key === "" || isNaN(index) || index !== expectedIndex) {
14
+ // Not an array-like object.
15
+ return object;
16
+ }
17
+ expectedIndex++;
18
+ }
19
+ return hasKeys ? Object.values(object) : object;
20
+ }
21
+
22
+ // Names of OS-generated files that should not be enumerated
23
+ export const hiddenFileNames = [".DS_Store"];
24
+
25
+ /**
26
+ * Return the Object prototype at the root of the object's prototype chain.
27
+ *
28
+ * This is used by functions like isPlainObject() to handle cases where the
29
+ * `Object` at the root prototype chain is in a different realm.
30
+ *
31
+ * @param {any} object
32
+ */
33
+ export function getRealmObjectPrototype(object) {
34
+ let proto = object;
35
+ while (Object.getPrototypeOf(proto) !== null) {
36
+ proto = Object.getPrototypeOf(proto);
37
+ }
38
+ return proto;
39
+ }
40
+
41
+ /**
42
+ * Return true if the object is a plain JavaScript object created by `{}`,
43
+ * `new Object()`, or `Object.create(null)`.
44
+ *
45
+ * This function also considers object-like things with no prototype (like a
46
+ * `Module`) as plain objects.
47
+ *
48
+ * @param {any} object
49
+ */
50
+ export function isPlainObject(object) {
51
+ // From https://stackoverflow.com/q/51722354/76472
52
+ if (typeof object !== "object" || object === null) {
53
+ return false;
54
+ }
55
+
56
+ // We treat object-like things with no prototype (like a Module) as plain
57
+ // objects.
58
+ if (Object.getPrototypeOf(object) === null) {
59
+ return true;
60
+ }
61
+
62
+ // Do we inherit directly from Object in this realm?
63
+ return Object.getPrototypeOf(object) === getRealmObjectPrototype(object);
64
+ }
65
+
66
+ /**
67
+ * Return true if the object is a string or object with a non-trival `toString`
68
+ * method.
69
+ *
70
+ * @param {any} obj
71
+ * @returns {obj is import("@weborigami/async-tree").StringLike}
72
+ */
73
+ export function isStringLike(obj) {
74
+ if (typeof obj === "string") {
75
+ return true;
76
+ } else if (obj?.toString === undefined) {
77
+ return false;
78
+ } else if (obj.toString === getRealmObjectPrototype(obj).toString) {
79
+ // The stupid Object.prototype.toString implementation always returns
80
+ // "[object Object]", so if that's the only toString method the object has,
81
+ // we return false.
82
+ return false;
83
+ } else {
84
+ return true;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Given a path like "/foo/bar/baz", return an array of keys like ["foo", "bar",
90
+ * "baz"].
91
+ *
92
+ * Leading slashes are ignored. Consecutive slashes or a trailing slash will be
93
+ * represented by the empty string.
94
+ *
95
+ * @param {string} pathname
96
+ */
97
+ export function keysFromPath(pathname) {
98
+ const keys = pathname.split("/");
99
+ if (keys[0] === "") {
100
+ // The path begins with a slash; drop that part.
101
+ keys.shift();
102
+ }
103
+ return keys;
104
+ }
105
+
106
+ // Used for natural sort order
107
+ export const naturalSortCompareFn = new Intl.Collator(undefined, {
108
+ numeric: true,
109
+ }).compare;
110
+
111
+ /**
112
+ * Sort the given array using [natural sort
113
+ * order](https://en.wikipedia.org/wiki/Natural_sort_order). Like the native
114
+ * `Array` `sort` function, this operation destructively modifies the array in
115
+ * place.
116
+ *
117
+ * The default sort order for some sources like operating system files can be
118
+ * unpredictable. Since it's quite common for file names to include numbers, it
119
+ * can helpful to use natural sort order instead: ["file1", "file9", "file10"]
120
+ * instead of ["file1", "file10", "file9"].
121
+ */
122
+ export function sortNatural(array) {
123
+ array.sort(naturalSortCompareFn);
124
+ }
@@ -0,0 +1,119 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import BrowserFileTree from "../src/BrowserFileTree.js";
4
+ import * as Tree from "../src/Tree.js";
5
+
6
+ // Skip these tests if we're not in a browser.
7
+ const isBrowser = typeof window !== "undefined";
8
+ if (isBrowser) {
9
+ describe("BrowserFileTree", async () => {
10
+ test("can get the keys of the tree", async () => {
11
+ const fixture = await createFixture();
12
+ assert.deepEqual(
13
+ [...(await fixture.keys())],
14
+ ["Alice.md", "Bob.md", "Carol.md"]
15
+ );
16
+ });
17
+
18
+ test("can get the value for a key", async () => {
19
+ const fixture = await createFixture();
20
+ assert.deepEqual(await strings(fixture), {
21
+ "Alice.md": "Hello, **Alice**.",
22
+ "Bob.md": "Hello, **Bob**.",
23
+ "Carol.md": "Hello, **Carol**.",
24
+ });
25
+ });
26
+
27
+ test("getting an unsupported key returns undefined", async () => {
28
+ const fixture = await createFixture();
29
+ assert.equal(await fixture.get("xyz"), undefined);
30
+ });
31
+
32
+ test("can set a value", async () => {
33
+ const fixture = await createFixture();
34
+
35
+ // Update existing key.
36
+ await fixture.set("Alice.md", "Goodbye, **Alice**.");
37
+
38
+ // New key.
39
+ await fixture.set("David.md", "Hello, **David**.");
40
+
41
+ // Delete key.
42
+ await fixture.set("Bob.md", undefined);
43
+
44
+ // Delete non-existent key.
45
+ await fixture.set("xyz", undefined);
46
+
47
+ assert.deepEqual(await strings(fixture), {
48
+ "Alice.md": "Goodbye, **Alice**.",
49
+ "Carol.md": "Hello, **Carol**.",
50
+ "David.md": "Hello, **David**.",
51
+ });
52
+ });
53
+
54
+ test("can create a subfolder via set", async () => {
55
+ const fixture = await createFixture();
56
+ const tree = {
57
+ async get(key) {
58
+ const name = key.replace(/\.md$/, "");
59
+ return `Hello, **${name}**.`;
60
+ },
61
+ async keys() {
62
+ return ["Ellen.md"];
63
+ },
64
+ };
65
+ await fixture.set("more", tree);
66
+ assert.deepEqual(await strings(fixture), {
67
+ "Alice.md": "Hello, **Alice**.",
68
+ "Bob.md": "Hello, **Bob**.",
69
+ "Carol.md": "Hello, **Carol**.",
70
+ more: {
71
+ "Ellen.md": "Hello, **Ellen**.",
72
+ },
73
+ });
74
+ });
75
+ });
76
+ }
77
+
78
+ async function createFile(directory, name, contents) {
79
+ const file = await directory.getFileHandle(name, { create: true });
80
+ const writable = await file.createWritable();
81
+ await writable.write(contents);
82
+ await writable.close();
83
+ }
84
+
85
+ let count = 0;
86
+ async function createFixture() {
87
+ const root = await navigator.storage.getDirectory();
88
+ const directory = await root.getDirectoryHandle("async-tree", {
89
+ create: true,
90
+ });
91
+
92
+ // Create a new subdirectory for each test.
93
+ const subdirectoryName = `test${count++}`;
94
+
95
+ // Delete any pre-existing subdirectory with that name.
96
+ try {
97
+ await directory.removeEntry(subdirectoryName, { recursive: true });
98
+ } catch (e) {
99
+ // Ignore errors.
100
+ }
101
+
102
+ const subdirectory = await directory.getDirectoryHandle(subdirectoryName, {
103
+ create: true,
104
+ });
105
+
106
+ await createFile(subdirectory, "Alice.md", "Hello, **Alice**.");
107
+ await createFile(subdirectory, "Bob.md", "Hello, **Bob**.");
108
+ await createFile(subdirectory, "Carol.md", "Hello, **Carol**.");
109
+
110
+ return new BrowserFileTree(subdirectory);
111
+ }
112
+
113
+ async function strings(tree) {
114
+ return Tree.plain(Tree.map(tree, (value) => text(value)));
115
+ }
116
+
117
+ function text(arrayBuffer) {
118
+ return new TextDecoder().decode(arrayBuffer);
119
+ }
@@ -0,0 +1,23 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import DeferredTree from "../src/DeferredTree.js";
4
+ import ObjectTree from "../src/ObjectTree.js";
5
+ import * as Tree from "../src/Tree.js";
6
+
7
+ describe("DeferredTree", () => {
8
+ test("lazy-loads a treelike object", async () => {
9
+ const tree = new DeferredTree(async () => ({ a: 1, b: 2, c: 3 }));
10
+ assert.deepEqual(await Tree.plain(tree), { a: 1, b: 2, c: 3 });
11
+ });
12
+
13
+ test("sets parent on subtrees", async () => {
14
+ const object = {
15
+ a: 1,
16
+ };
17
+ const parent = new ObjectTree({});
18
+ const fixture = new DeferredTree(() => object);
19
+ fixture.parent = parent;
20
+ const tree = await fixture.tree();
21
+ assert.equal(tree.parent, parent);
22
+ });
23
+ });
@@ -0,0 +1,134 @@
1
+ import assert from "node:assert";
2
+ import * as fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { describe, test } from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import FileTree from "../src/FileTree.js";
7
+ import * as Tree from "../src/Tree.js";
8
+
9
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const tempDirectory = path.join(dirname, "fixtures/temp");
11
+
12
+ describe("FileTree", async () => {
13
+ test("can get the keys of the tree", async () => {
14
+ const fixture = createFixture("fixtures/markdown");
15
+ assert.deepEqual(
16
+ [...(await fixture.keys())],
17
+ ["Alice.md", "Bob.md", "Carol.md"]
18
+ );
19
+ });
20
+
21
+ test("can get the value for a key", async () => {
22
+ const fixture = createFixture("fixtures/markdown");
23
+ const alice = await fixture.get("Alice.md");
24
+ assert.equal(alice, "Hello, **Alice**.");
25
+ });
26
+
27
+ test("getting an unsupported key returns undefined", async () => {
28
+ const fixture = createFixture("fixtures/markdown");
29
+ assert.equal(await fixture.get("xyz"), undefined);
30
+ });
31
+
32
+ test("sets parent on subtrees", async () => {
33
+ const fixture = createFixture("fixtures");
34
+ const markdown = await fixture.get("markdown");
35
+ assert.equal(markdown.parent, fixture);
36
+ });
37
+
38
+ test("can indicate which values are subtrees", async () => {
39
+ const fixture = createFixture("fixtures");
40
+ assert(await fixture.isKeyForSubtree("markdown"));
41
+ const markdown = await fixture.get("markdown");
42
+ assert(!(await markdown.isKeyForSubtree("a.txt")));
43
+ });
44
+
45
+ test("can write out a file via set()", async () => {
46
+ await createTempDirectory();
47
+
48
+ // Write out a file.
49
+ const fileName = "file1";
50
+ const fileText = "This is the first file.";
51
+ const tempFiles = new FileTree(tempDirectory);
52
+ await tempFiles.set(fileName, fileText);
53
+
54
+ // Read it back in.
55
+ const filePath = path.join(tempDirectory, fileName);
56
+ const actualText = String(await fs.readFile(filePath));
57
+
58
+ assert.equal(fileText, actualText);
59
+
60
+ await removeTempDirectory();
61
+ });
62
+
63
+ test("can write out subfolder via set()", async () => {
64
+ await createTempDirectory();
65
+
66
+ // Create a tiny set of "files".
67
+ const obj = {
68
+ file1: "This is the first file.",
69
+ subfolder: {
70
+ file2: "This is the second file.",
71
+ },
72
+ };
73
+
74
+ // Write out files as a new folder called "folder".
75
+ const tempFiles = new FileTree(tempDirectory);
76
+ await tempFiles.set("folder", obj);
77
+
78
+ // Read them back in.
79
+ const actualFiles = await tempFiles.get("folder");
80
+ const strings = Tree.map(actualFiles, (buffer) => String(buffer));
81
+ const plain = await Tree.plain(strings);
82
+ assert.deepEqual(plain, obj);
83
+
84
+ await removeTempDirectory();
85
+ });
86
+
87
+ test("can delete a file via set()", async () => {
88
+ await createTempDirectory();
89
+ const tempFile = path.join(tempDirectory, "file");
90
+ await fs.writeFile(tempFile, "");
91
+ const tempFiles = new FileTree(tempDirectory);
92
+ await tempFiles.set("file", undefined);
93
+ let stats;
94
+ try {
95
+ stats = await fs.stat(tempFile);
96
+ } catch (/** @type {any} */ error) {
97
+ if (error.code !== "ENOENT") {
98
+ throw error;
99
+ }
100
+ }
101
+ assert(stats === undefined);
102
+ await removeTempDirectory();
103
+ });
104
+
105
+ test("can delete a folder via set()", async () => {
106
+ await createTempDirectory();
107
+ const folder = path.join(tempDirectory, "folder");
108
+ await fs.mkdir(folder);
109
+ const tempFiles = new FileTree(tempDirectory);
110
+ await tempFiles.set("folder", undefined);
111
+ let stats;
112
+ try {
113
+ stats = await fs.stat(folder);
114
+ } catch (/** @type {any} */ error) {
115
+ if (error.code !== "ENOENT") {
116
+ throw error;
117
+ }
118
+ }
119
+ assert(stats === undefined);
120
+ await removeTempDirectory();
121
+ });
122
+ });
123
+
124
+ function createFixture(fixturePath) {
125
+ return new FileTree(path.join(dirname, fixturePath));
126
+ }
127
+
128
+ async function createTempDirectory() {
129
+ await fs.mkdir(tempDirectory, { recursive: true });
130
+ }
131
+
132
+ async function removeTempDirectory() {
133
+ await fs.rm(tempDirectory, { recursive: true });
134
+ }
@@ -0,0 +1,51 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import FunctionTree from "../src/FunctionTree.js";
4
+
5
+ describe("FunctionTree", async () => {
6
+ test("can get the keys of the tree", async () => {
7
+ const fixture = createFixture();
8
+ assert.deepEqual(
9
+ [...(await fixture.keys())],
10
+ ["Alice.md", "Bob.md", "Carol.md"]
11
+ );
12
+ });
13
+
14
+ test("can get the value for a key", async () => {
15
+ const fixture = createFixture();
16
+ const alice = await fixture.get("Alice.md");
17
+ assert.equal(alice, "Hello, **Alice**.");
18
+ });
19
+
20
+ test("unpacking the tree returns the function itself", async () => {
21
+ const fn = () => true;
22
+ const fixture = new FunctionTree(fn);
23
+ assert.equal(await fixture.unpack(), fn);
24
+ });
25
+
26
+ test("getting a value from function with multiple arguments curries the function", async () => {
27
+ const fixture = new FunctionTree((a, b, c) => a + b + c);
28
+ const fnA = await fixture.get(1);
29
+ const fnAB = await fnA.get(2);
30
+ const result = await fnAB.get(3);
31
+ assert.equal(result, 6);
32
+ });
33
+
34
+ test("getting an unsupported key returns undefined", async () => {
35
+ const fixture = createFixture();
36
+ assert.equal(await fixture.get("xyz"), undefined);
37
+ });
38
+ });
39
+
40
+ function createFixture() {
41
+ return new FunctionTree(
42
+ (key) => {
43
+ if (key?.endsWith?.(".md")) {
44
+ const name = key.slice(0, -3);
45
+ return `Hello, **${name}**.`;
46
+ }
47
+ return undefined;
48
+ },
49
+ ["Alice.md", "Bob.md", "Carol.md"]
50
+ );
51
+ }
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import MapTree from "../src/MapTree.js";
4
+
5
+ describe("MapTree", () => {
6
+ test("can get the keys of the tree", async () => {
7
+ const fixture = createFixture();
8
+ assert.deepEqual([...(await fixture.keys())], ["a", "b", "c"]);
9
+ });
10
+
11
+ test("can get the value for a key", async () => {
12
+ const fixture = createFixture();
13
+ const a = await fixture.get("a");
14
+ assert.equal(a, 1);
15
+ });
16
+
17
+ test("sets parent on subtrees", async () => {
18
+ const map = new Map([["more", new Map([["a", 1]])]]);
19
+ const fixture = new MapTree(map);
20
+ const more = await fixture.get("more");
21
+ assert.equal(more.parent, fixture);
22
+ });
23
+
24
+ test("getting an unsupported key returns undefined", async () => {
25
+ const fixture = createFixture();
26
+ assert.equal(await fixture.get("d"), undefined);
27
+ });
28
+ });
29
+
30
+ function createFixture() {
31
+ const map = new Map([
32
+ ["a", 1],
33
+ ["b", 2],
34
+ ["c", 3],
35
+ ]);
36
+ return new MapTree(map);
37
+ }
@@ -0,0 +1,159 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import ObjectTree from "../src/ObjectTree.js";
4
+ import * as Tree from "../src/Tree.js";
5
+
6
+ describe("ObjectTree", () => {
7
+ test("can get the keys of the tree", async () => {
8
+ const fixture = createFixture();
9
+ assert.deepEqual(
10
+ [...(await fixture.keys())],
11
+ ["Alice.md", "Bob.md", "Carol.md"]
12
+ );
13
+ });
14
+
15
+ test("can get the value for a key", async () => {
16
+ const fixture = createFixture();
17
+ const alice = await fixture.get("Alice.md");
18
+ assert.equal(alice, "Hello, **Alice**.");
19
+ });
20
+
21
+ test("getting an unsupported key returns undefined", async () => {
22
+ const fixture = createFixture();
23
+ assert.equal(await fixture.get("xyz"), undefined);
24
+ });
25
+
26
+ test("can set a value", async () => {
27
+ const tree = new ObjectTree({
28
+ a: 1,
29
+ b: 2,
30
+ c: 3,
31
+ });
32
+
33
+ // Update existing key.
34
+ await tree.set("a", 4);
35
+
36
+ // New key.
37
+ await tree.set("d", 5);
38
+
39
+ // Delete key.
40
+ await tree.set("b", undefined);
41
+
42
+ assert.deepEqual(await Tree.entries(tree), [
43
+ ["a", 4],
44
+ ["c", 3],
45
+ ["d", 5],
46
+ ]);
47
+ });
48
+
49
+ test("can wrap a class instance", async () => {
50
+ class Foo {
51
+ constructor() {
52
+ this.a = 1;
53
+ }
54
+
55
+ get prop() {
56
+ return this._prop;
57
+ }
58
+ set prop(prop) {
59
+ this._prop = prop;
60
+ }
61
+ }
62
+ class Bar extends Foo {
63
+ method() {}
64
+ }
65
+ const bar = new Bar();
66
+ /** @type {any} */ (bar).extra = "Hello";
67
+ const fixture = new ObjectTree(bar);
68
+ assert.deepEqual(await Tree.entries(fixture), [
69
+ ["a", 1],
70
+ ["extra", "Hello"],
71
+ ["prop", undefined],
72
+ ]);
73
+ assert.equal(await fixture.get("a"), 1);
74
+ await fixture.set("prop", "Goodbye");
75
+ assert.equal(bar.prop, "Goodbye");
76
+ assert.equal(await fixture.get("prop"), "Goodbye");
77
+ });
78
+
79
+ test("creates an ObjectTree for subtrees", async () => {
80
+ const object = {
81
+ a: 1,
82
+ more: {
83
+ b: 2,
84
+ },
85
+ };
86
+ const fixture = new ObjectTree(object);
87
+ const more = await fixture.get("more");
88
+ assert.equal(more.constructor, ObjectTree);
89
+ const b = await more.get("b");
90
+ assert.equal(b, 2);
91
+ });
92
+
93
+ test("sets parent on subtrees", async () => {
94
+ const object = {
95
+ a: 1,
96
+ more: {
97
+ b: 2,
98
+ },
99
+ };
100
+ const fixture = new ObjectTree(object);
101
+ const more = await fixture.get("more");
102
+ assert.equal(more.parent, fixture);
103
+ });
104
+
105
+ test("isKeyForSubtree() indicates which values are subtrees", async () => {
106
+ const tree = new ObjectTree({
107
+ a1: 1,
108
+ a2: {
109
+ b1: 2,
110
+ },
111
+ a3: 3,
112
+ a4: {
113
+ b2: 4,
114
+ },
115
+ });
116
+ const keys = Array.from(await tree.keys());
117
+ const subtrees = await Promise.all(
118
+ keys.map(async (key) => await tree.isKeyForSubtree(key))
119
+ );
120
+ assert.deepEqual(subtrees, [false, true, false, true]);
121
+ });
122
+
123
+ test("returns an ObjectTree for value that's a plain sub-object or sub-array", async () => {
124
+ const tree = new ObjectTree({
125
+ a: 1,
126
+ object: {
127
+ b: 2,
128
+ },
129
+ array: [3],
130
+ });
131
+
132
+ const object = await tree.get("object");
133
+ assert.equal(object instanceof ObjectTree, true);
134
+ assert.deepEqual(await Tree.plain(object), { b: 2 });
135
+
136
+ const array = await tree.get("array");
137
+ assert.equal(array instanceof ObjectTree, true);
138
+ assert.deepEqual(await Tree.plain(array), [3]);
139
+ });
140
+
141
+ test("returns an async tree value as is", async () => {
142
+ const subtree = {
143
+ async get(key) {},
144
+ async keys() {},
145
+ };
146
+ const tree = new ObjectTree({
147
+ subtree,
148
+ });
149
+ assert.equal(await tree.get("subtree"), subtree);
150
+ });
151
+ });
152
+
153
+ function createFixture() {
154
+ return new ObjectTree({
155
+ "Alice.md": "Hello, **Alice**.",
156
+ "Bob.md": "Hello, **Bob**.",
157
+ "Carol.md": "Hello, **Carol**.",
158
+ });
159
+ }
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import SetTree from "../src/SetTree.js";
4
+
5
+ describe("SetTree", () => {
6
+ test("can get the keys of the tree", async () => {
7
+ const set = new Set(["a", "b", "c"]);
8
+ const fixture = new SetTree(set);
9
+ assert.deepEqual([...(await fixture.keys())], [0, 1, 2]);
10
+ });
11
+
12
+ test("can get the value for a key", async () => {
13
+ const set = new Set(["a", "b", "c"]);
14
+ const fixture = new SetTree(set);
15
+ const a = await fixture.get(0);
16
+ assert.equal(a, "a");
17
+ });
18
+
19
+ test("getting an unsupported key returns undefined", async () => {
20
+ const set = new Set(["a", "b", "c"]);
21
+ const fixture = new SetTree(set);
22
+ assert.equal(await fixture.get(3), undefined);
23
+ });
24
+
25
+ test("sets parent on subtrees", async () => {
26
+ const set = new Set();
27
+ set.add(new Set("a"));
28
+ const fixture = new SetTree(set);
29
+ const subtree = await fixture.get(0);
30
+ assert.equal(subtree.parent, fixture);
31
+ });
32
+ });