import assert from "node:assert"; import { describe, test } from "node:test"; import MapTree from "../src/drivers/MapTree.js"; import { DeepObjectTree, ObjectTree, Tree } from "../src/internal.js"; import * as symbols from "../src/symbols.js"; describe("Tree", () => { test("assign applies one tree to another", async () => { const target = new DeepObjectTree({ a: 1, b: 2, more: { d: 3, }, }); const source = new DeepObjectTree({ a: 4, // Overwrite existing value b: undefined, // Delete c: 5, // Add more: { // Should leave existing `more` keys alone. e: 6, // Add }, // Add new subtree extra: { f: 7, }, }); // Apply changes. const result = await Tree.assign(target, source); assert.equal(result, target); const plain = await Tree.plain(target); assert.deepEqual(plain, { a: 4, c: 5, more: { d: 3, e: 6, }, extra: { f: 7, }, }); }); test("assign() can apply updates to an array", async () => { const target = new ObjectTree(["a", "b", "c"]); await Tree.assign(target, ["d", "e"]); assert.deepEqual(await Tree.plain(target), ["d", "e", "c"]); }); test("clear() removes all values", async () => { const fixture = createFixture(); await Tree.clear(fixture); assert.deepEqual(Array.from(await Tree.entries(fixture)), []); }); test("entries() returns the [key, value] pairs", async () => { const fixture = createFixture(); assert.deepEqual(Array.from(await Tree.entries(fixture)), [ ["Alice.md", "Hello, **Alice**."], ["Bob.md", "Hello, **Bob**."], ["Carol.md", "Hello, **Carol**."], ]); }); test("forEach() invokes a callback for each entry", async () => { const fixture = createFixture(); const results = {}; await Tree.forEach(fixture, async (value, key) => { results[key] = value; }); assert.deepEqual(results, { "Alice.md": "Hello, **Alice**.", "Bob.md": "Hello, **Bob**.", "Carol.md": "Hello, **Carol**.", }); }); test("from() returns an async tree as is", async () => { const tree1 = new ObjectTree({ a: "Hello, a.", }); const tree2 = Tree.from(tree1); assert.equal(tree2, tree1); }); test("from() uses an object's unpack() method if defined", async () => { const obj = new String(); /** @type {any} */ (obj).unpack = () => ({ a: "Hello, a.", }); const tree = Tree.from(obj); assert.deepEqual(await Tree.plain(tree), { a: "Hello, a.", }); }); test("from returns a deep object tree if deep option is true", async () => { const obj = { sub: { a: 1, }, }; const tree = Tree.from(obj, { deep: true }); assert(tree instanceof DeepObjectTree); }); test("from returns a deep object tree if object has [deep] symbol set", async () => { const obj = { sub: { a: 1, }, }; Object.defineProperty(obj, symbols.deep, { value: true }); const tree = Tree.from(obj); assert(tree instanceof DeepObjectTree); }); test("from() creates a deferred tree if unpack() returns a promise", async () => { const obj = new String(); /** @type {any} */ (obj).unpack = async () => ({ a: "Hello, a.", }); const tree = Tree.from(obj); assert.deepEqual(await Tree.plain(tree), { a: "Hello, a.", }); }); test("from() autoboxes primitive values", async () => { const tree = Tree.from("Hello, world."); const slice = await tree.get("slice"); const result = await slice(0, 5); assert.equal(result, "Hello"); }); test("has returns true if the key exists", async () => { const fixture = createFixture(); assert.equal(await Tree.has(fixture, "Alice.md"), true); assert.equal(await Tree.has(fixture, "David.md"), false); }); test("isAsyncTree returns true if the object is a tree", () => { const missingGetAndKeys = {}; assert(!Tree.isAsyncTree(missingGetAndKeys)); const missingIterator = { async get() {}, }; assert(!Tree.isAsyncTree(missingIterator)); const missingGet = { async keys() {}, }; assert(!Tree.isAsyncTree(missingGet)); const hasGetAndKeys = { async get() {}, async keys() {}, }; assert(Tree.isAsyncTree(hasGetAndKeys)); }); test("isAsyncMutableTree returns true if the object is a mutable tree", () => { assert.equal( Tree.isAsyncMutableTree({ get() {}, keys() {}, }), false ); assert.equal(Tree.isAsyncMutableTree(createFixture()), true); }); test("isTreelike() returns true if the argument can be cast to an async tree", () => { assert(!Tree.isTreelike(null)); assert(Tree.isTreelike({})); assert(Tree.isTreelike([])); assert(Tree.isTreelike(new Map())); assert(Tree.isTreelike(new Set())); }); test("map() maps values", async () => { const tree = new DeepObjectTree({ a: "Alice", more: { b: "Bob", }, }); const mapped = Tree.map(tree, { deep: true, value: (value) => value.toUpperCase(), }); assert.deepEqual(await Tree.plain(mapped), { a: "ALICE", more: { b: "BOB", }, }); }); test("mapReduce() can map values and reduce them", async () => { const tree = new DeepObjectTree({ a: 1, b: 2, more: { c: 3, }, d: 4, }); const reduced = await Tree.mapReduce( tree, (value) => value, async (values) => String.prototype.concat(...values) ); assert.deepEqual(reduced, "1234"); }); test("paths returns an array of paths to the values in the tree", async () => { const tree = new DeepObjectTree({ a: 1, b: 2, c: { d: 3, e: 4, }, }); assert.deepEqual(await Tree.paths(tree), ["a", "b", "c/d", "c/e"]); }); test("plain() produces a plain object version of a tree", async () => { const tree = new ObjectTree({ a: 1, // Slashes should be normalized "sub1/": { b: 2, }, sub2: { c: 3, }, }); const plain = await Tree.plain(tree); assert.deepEqual(plain, { a: 1, sub1: { b: 2, }, sub2: { c: 3, }, }); }); test("plain() produces an array for an array-like tree", async () => { const original = ["a", "b", "c"]; const tree = new ObjectTree(original); const plain = await Tree.plain(tree); assert.deepEqual(plain, original); }); test("plain() leaves an array-like tree as an object if keys aren't consecutive", async () => { const original = { 0: "a", 1: "b", // missing 3: "c", }; const tree = new ObjectTree(original); const plain = await Tree.plain(tree); assert.deepEqual(plain, original); }); test("plain() returns empty array or object for ObjectTree as necessary", async () => { const tree = new ObjectTree({}); assert.deepEqual(await Tree.plain(tree), {}); const arrayTree = new ObjectTree([]); assert.deepEqual(await Tree.plain(arrayTree), []); }); test("plain() awaits async properties", async () => { const object = { get name() { return Promise.resolve("Alice"); }, }; assert.deepEqual(await Tree.plain(object), { name: "Alice" }); }); test("plain() coerces TypedArray values to strings", async () => { const tree = new ObjectTree({ a: new TextEncoder().encode("Hello, world."), }); const plain = await Tree.plain(tree); assert.equal(plain.a, "Hello, world."); }); test("remove method removes a value", async () => { const fixture = createFixture(); await Tree.remove(fixture, "Alice.md"); assert.deepEqual(Array.from(await Tree.entries(fixture)), [ ["Bob.md", "Hello, **Bob**."], ["Carol.md", "Hello, **Carol**."], ]); }); test("toFunction returns a function that invokes a tree's get() method", async () => { const tree = new ObjectTree({ a: 1, b: 2, }); const fn = Tree.toFunction(tree); assert.equal(await fn("a"), 1); assert.equal(await fn("b"), 2); }); test("traverse() a path of keys", async () => { const tree = new ObjectTree({ a1: 1, a2: { b1: 2, b2: { c1: 3, c2: 4, }, }, }); assert.equal(await Tree.traverse(tree), tree); assert.equal(await Tree.traverse(tree, "a1"), 1); assert.equal(await Tree.traverse(tree, "a2", "b2", "c2"), 4); assert.equal( await Tree.traverse(tree, "a2", "doesntexist", "c2"), undefined ); }); test("traverse() a function with fixed number of arguments", async () => { const tree = (a, b) => ({ c: "Result", }); assert.equal(await Tree.traverse(tree, "a", "b", "c"), "Result"); }); test("traverse() from one tree into another", async () => { const tree = new ObjectTree({ a: { b: new MapTree([ ["c", "Hello"], ["d", "Goodbye"], ]), }, }); assert.equal(await Tree.traverse(tree, "a", "b", "c"), "Hello"); }); test("traversePath() traverses a slash-separated path", async () => { const tree = new ObjectTree({ a: { b: { c: "Hello", }, }, }); assert.equal(await Tree.traversePath(tree, "a/b/c"), "Hello"); }); test("values() returns the store's values", async () => { const fixture = createFixture(); assert.deepEqual(Array.from(await Tree.values(fixture)), [ "Hello, **Alice**.", "Hello, **Bob**.", "Hello, **Carol**.", ]); }); }); function createFixture() { return new ObjectTree({ "Alice.md": "Hello, **Alice**.", "Bob.md": "Hello, **Bob**.", "Carol.md": "Hello, **Carol**.", }); }