1
0

run npm install to generate a package lock

This commit is contained in:
sashinexists
2024-12-07 13:18:31 +11:00
parent e7d08a91b5
commit 23437d228e
2501 changed files with 290663 additions and 0 deletions

386
node_modules/@weborigami/async-tree/test/Tree.test.js generated vendored Normal file
View File

@@ -0,0 +1,386 @@
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**.",
});
}

View File

@@ -0,0 +1,54 @@
/**
* A simple test runner for the browser to run the subset of the Node.s test
* runner used by the project.
*/
export default function assert(condition) {
if (!condition) {
throw new Error("Assertion failed");
}
}
assert.equal = (actual, expected) => {
if (Number.isNaN(actual) && Number.isNaN(expected)) {
return;
} else if (actual == expected) {
return;
} else {
throw new Error(`Expected ${expected} but got ${actual}`);
}
};
// This is a simplified deepEqual test that examines the conditions we care
// about. For reference, the actual Node assert.deepEqual is much more complex:
// see https://github.com/nodejs/node/blob/main/lib/internal/util/comparisons.js
assert.deepEqual = (actual, expected) => {
if (actual === expected) {
return;
} else if (
typeof actual === "object" &&
actual != null &&
typeof expected === "object" &&
expected != null &&
Object.keys(actual).length === Object.keys(expected).length
) {
for (const prop in actual) {
if (!expected.hasOwnProperty(prop)) {
break;
}
assert.deepEqual(actual[prop], expected[prop]);
}
return;
}
throw new Error(`Expected ${expected} but got ${actual}`);
};
assert.rejects = async (promise) => {
try {
await promise;
throw new Error("Expected promise to reject but it resolved");
} catch (error) {
return;
}
};

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<script type="importmap">
{
"imports": {
"node:assert": "./assert.js",
"node:test": "./testRunner.js"
}
}
</script>
<!-- Omit FileTree.test.js, which is Node.js only -->
<!-- Omit SiteTree.test.js, which requires mocks -->
<script type="module" src="../BrowserFileTree.test.js"></script>
<script type="module" src="../DeferredTree.test.js"></script>
<script type="module" src="../FunctionTree.test.js"></script>
<script type="module" src="../MapTree.test.js"></script>
<script type="module" src="../ObjectTree.test.js"></script>
<script type="module" src="../SetTree.test.js"></script>
<script type="module" src="../Tree.test.js"></script>
<script type="module" src="../operations/cache.test.js"></script>
<script type="module" src="../operations/merge.test.js"></script>
<script type="module" src="../operations/deepMerge.test.js"></script>
<script type="module" src="../transforms/cachedKeyMaps.test.js"></script>
<script
type="module"
src="../transforms/keyFunctionsForExtensions.test.js"
></script>
<script type="module" src="../transforms/mapFn.test.js"></script>
<script type="module" src="../utilities.test.js"></script>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,51 @@
/**
* A simple test runner for the browser to run the subset of the Node.s test
* runner used by the project.
*/
let promises = {};
let currentSuite;
const markers = {
success: "✅",
skipped: "ー",
fail: "❌",
};
export async function describe(name, fn) {
promises[name] = [];
currentSuite = name;
await fn();
const results = await Promise.all(promises[name]);
const someFailed = results.some((result) => result.result === "fail");
const header = `${someFailed ? markers.fail : markers.success} ${name}`;
console[someFailed ? "group" : "groupCollapsed"](header);
for (const result of results) {
const marker = markers[result.result];
const name = result.name;
const message = result.result === "fail" ? `: ${result.message}` : "";
const skipped = result.result === "skipped" ? " [skipped]" : "";
console.log(`${marker} ${name}${message}${skipped}`);
}
console.groupEnd();
}
// Node test() calls can call an async function, but the test() function isn't
// declared async. We implicitly wrap the test call with a Promise and add it to
// the list of promises for the current suite.
export async function test(name, fn) {
promises[currentSuite].push(runTest(name, fn));
}
test.skip = (name, fn) => {
promises[currentSuite].push(Promise.resolve({ result: "skipped", name }));
};
async function runTest(name, fn) {
try {
await fn();
return { result: "success", name };
} catch (/** @type {any} */ error) {
return { result: "fail", name, message: error.message };
}
}

View File

@@ -0,0 +1,153 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import BrowserFileTree from "../../src/drivers/BrowserFileTree.js";
import { Tree } from "../../src/internal.js";
// Skip these tests if we're not in a browser.
const isBrowser = typeof window !== "undefined";
if (isBrowser) {
describe("BrowserFileTree", async () => {
test("can get the keys of the tree", async () => {
const fixture = await createFixture();
assert.deepEqual(Array.from(await fixture.keys()), [
"Alice.md",
"Bob.md",
"Carol.md",
"subfolder/",
]);
});
test("can get the value for a key", async () => {
const fixture = await createFixture();
const buffer = await fixture.get("Alice.md");
assert.equal(text(buffer), "Hello, **Alice**.");
});
test("getting an unsupported key returns undefined", async () => {
const fixture = await createFixture();
assert.equal(await fixture.get("xyz"), undefined);
});
test("getting empty key returns undefined", async () => {
const fixture = await createFixture();
assert.equal(await fixture.get(""), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const fixture = await createFixture();
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
test("sets parent on subtrees", async () => {
const fixture = await createFixture();
const subfolder = await fixture.get("subfolder");
assert.equal(subfolder.parent, fixture);
});
test("can retrieve values with optional trailing slash", async () => {
const fixture = await createFixture();
assert(await fixture.get("Alice.md"));
assert(await fixture.get("Alice.md/"));
assert(await fixture.get("subfolder"));
assert(await fixture.get("subfolder/"));
});
test("can set a value", async () => {
const fixture = await createFixture();
// Update existing key.
await fixture.set("Alice.md", "Goodbye, **Alice**.");
// New key.
await fixture.set("David.md", "Hello, **David**.");
// Delete key.
await fixture.set("Bob.md", undefined);
// Delete non-existent key.
await fixture.set("xyz", undefined);
assert.deepEqual(await strings(fixture), {
"Alice.md": "Goodbye, **Alice**.",
"Carol.md": "Hello, **Carol**.",
"David.md": "Hello, **David**.",
subfolder: {},
});
});
test("can create a subfolder via set", async () => {
const fixture = await createFixture();
const tree = {
async get(key) {
const name = key.replace(/\.md$/, "");
return `Hello, **${name}**.`;
},
async keys() {
return ["Ellen.md"];
},
};
await fixture.set("more", tree);
assert.deepEqual(await strings(fixture), {
"Alice.md": "Hello, **Alice**.",
"Bob.md": "Hello, **Bob**.",
"Carol.md": "Hello, **Carol**.",
more: {
"Ellen.md": "Hello, **Ellen**.",
},
subfolder: {},
});
});
});
}
async function createFile(directory, name, contents) {
const file = await directory.getFileHandle(name, { create: true });
const writable = await file.createWritable();
await writable.write(contents);
await writable.close();
}
let count = 0;
async function createFixture() {
const root = await navigator.storage.getDirectory();
const directory = await root.getDirectoryHandle("async-tree", {
create: true,
});
// Create a new subdirectory for each test.
const subdirectoryName = `test${count++}`;
// Delete any pre-existing subdirectory with that name.
try {
await directory.removeEntry(subdirectoryName, { recursive: true });
} catch (e) {
// Ignore errors.
}
const subdirectory = await directory.getDirectoryHandle(subdirectoryName, {
create: true,
});
await createFile(subdirectory, "Alice.md", "Hello, **Alice**.");
await createFile(subdirectory, "Bob.md", "Hello, **Bob**.");
await createFile(subdirectory, "Carol.md", "Hello, **Carol**.");
await subdirectory.getDirectoryHandle("subfolder", {
create: true,
});
return new BrowserFileTree(subdirectory);
}
async function strings(tree) {
return Tree.plain(Tree.map(tree, (value) => text(value)));
}
function text(arrayBuffer) {
return new TextDecoder().decode(arrayBuffer);
}

View File

@@ -0,0 +1,17 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import DeepMapTree from "../../src/drivers/DeepMapTree.js";
import { Tree } from "../../src/internal.js";
describe("DeepMapTree", () => {
test("returns a DeepMapTree for value that's a Map", async () => {
const tree = new DeepMapTree([
["a", 1],
["map", new Map([["b", 2]])],
]);
const map = await tree.get("map");
assert.equal(map instanceof DeepMapTree, true);
assert.deepEqual(await Tree.plain(map), { b: 2 });
assert.equal(map.parent, tree);
});
});

View File

@@ -0,0 +1,35 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, Tree } from "../../src/internal.js";
describe("DeepObjectTree", () => {
test("returns an ObjectTree for value that's a plain sub-object or sub-array", async () => {
const tree = createFixture();
const object = await tree.get("object");
assert.equal(object instanceof DeepObjectTree, true);
assert.deepEqual(await Tree.plain(object), { b: 2 });
assert.equal(object.parent, tree);
const array = await tree.get("array");
assert.equal(array instanceof DeepObjectTree, true);
assert.deepEqual(await Tree.plain(array), [3]);
assert.equal(array.parent, tree);
});
test("adds trailing slashes to keys for subtrees including plain objects or arrays", async () => {
const tree = createFixture();
const keys = Array.from(await tree.keys());
assert.deepEqual(keys, ["a", "object/", "array/"]);
});
});
function createFixture() {
return new DeepObjectTree({
a: 1,
object: {
b: 2,
},
array: [3],
});
}

View File

@@ -0,0 +1,22 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import DeferredTree from "../../src/drivers/DeferredTree.js";
import { ObjectTree, Tree } from "../../src/internal.js";
describe("DeferredTree", () => {
test("lazy-loads a treelike object", async () => {
const tree = new DeferredTree(async () => ({ a: 1, b: 2, c: 3 }));
assert.deepEqual(await Tree.plain(tree), { a: 1, b: 2, c: 3 });
});
test("sets parent on subtrees", async () => {
const object = {
a: 1,
};
const parent = new ObjectTree({});
const fixture = new DeferredTree(() => object);
fixture.parent = parent;
const tree = await fixture.tree();
assert.equal(tree.parent, parent);
});
});

View File

@@ -0,0 +1,116 @@
import assert from "node:assert";
import { beforeEach, describe, mock, test } from "node:test";
import ExplorableSiteTree from "../../src/drivers/ExplorableSiteTree.js";
import { Tree } from "../../src/internal.js";
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
const mockHost = "https://mock";
const mockResponses = {
"/.keys.json": {
data: JSON.stringify(["about/", "index.html"]),
},
"/about": {
redirected: true,
status: 301,
url: "https://mock/about/",
},
"/about/.keys.json": {
data: JSON.stringify(["Alice.html", "Bob.html", "Carol.html"]),
},
"/about/Alice.html": {
data: "Hello, Alice!",
},
"/about/Bob.html": {
data: "Hello, Bob!",
},
"/about/Carol.html": {
data: "Hello, Carol!",
},
"/index.html": {
data: "Home page",
},
};
describe("ExplorableSiteTree", () => {
beforeEach(() => {
mock.method(global, "fetch", mockFetch);
});
test("can get the keys of a tree", async () => {
const fixture = new ExplorableSiteTree(mockHost);
const keys = await fixture.keys();
assert.deepEqual(Array.from(keys), ["about/", "index.html"]);
});
test("can get a plain value for a key", async () => {
const fixture = new ExplorableSiteTree(mockHost);
const arrayBuffer = await fixture.get("index.html");
const text = textDecoder.decode(arrayBuffer);
assert.equal(text, "Home page");
});
test("getting an unsupported key returns undefined", async () => {
const fixture = new ExplorableSiteTree(mockHost);
assert.equal(await fixture.get("xyz"), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const fixture = new ExplorableSiteTree(mockHost);
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
test("can return a new tree for a key that redirects", async () => {
const fixture = new ExplorableSiteTree(mockHost);
const about = await fixture.get("about");
assert(about instanceof ExplorableSiteTree);
assert.equal(about.href, "https://mock/about/");
});
test("can convert a site to a plain object", async () => {
const fixture = new ExplorableSiteTree(mockHost);
// Convert buffers to strings.
const strings = Tree.map(fixture, {
deep: true,
value: (value) => textDecoder.decode(value),
});
assert.deepEqual(await Tree.plain(strings), {
about: {
"Alice.html": "Hello, Alice!",
"Bob.html": "Hello, Bob!",
"Carol.html": "Hello, Carol!",
},
"index.html": "Home page",
});
});
});
async function mockFetch(href) {
if (!href.startsWith(mockHost)) {
return { status: 404 };
}
const path = href.slice(mockHost.length);
const mockedResponse = mockResponses[path];
if (mockedResponse) {
return Object.assign(
{
arrayBuffer: () => textEncoder.encode(mockedResponse.data).buffer,
ok: true,
status: 200,
text: () => mockedResponse.data,
},
mockedResponse
);
}
return {
ok: false,
status: 404,
};
}

View File

@@ -0,0 +1,192 @@
import assert from "node:assert";
import * as fs from "node:fs/promises";
import path from "node:path";
import { describe, test } from "node:test";
import { fileURLToPath } from "node:url";
import FileTree from "../../src/drivers/FileTree.js";
import { ObjectTree, Tree } from "../../src/internal.js";
const dirname = path.dirname(fileURLToPath(import.meta.url));
const tempDirectory = path.join(dirname, "fixtures/temp");
const textDecoder = new TextDecoder();
describe("FileTree", async () => {
test("can get the keys of the tree", async () => {
const fixture = createFixture("fixtures/markdown");
assert.deepEqual(Array.from(await fixture.keys()), [
"Alice.md",
"Bob.md",
"Carol.md",
"subfolder/",
]);
});
test("can get the value for a key", async () => {
const fixture = createFixture("fixtures/markdown");
const buffer = await fixture.get("Alice.md");
const text = textDecoder.decode(buffer);
assert.equal(text, "Hello, **Alice**.");
});
test("getting an unsupported key returns undefined", async () => {
const fixture = createFixture("fixtures/markdown");
assert.equal(await fixture.get("xyz"), undefined);
});
test("getting empty key returns undefined", async () => {
const fixture = createFixture("fixtures/markdown");
assert.equal(await fixture.get(""), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const fixture = createFixture("fixtures/markdown");
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
test("can retrieve values with optional trailing slash", async () => {
const fixture = createFixture("fixtures/markdown");
assert(await fixture.get("Alice.md"));
assert(await fixture.get("Alice.md/"));
assert(await fixture.get("subfolder"));
assert(await fixture.get("subfolder/"));
});
test("sets parent on subtrees", async () => {
const fixture = createFixture("fixtures");
const markdown = await fixture.get("markdown");
assert.equal(markdown.parent, fixture);
});
test("can write out a file via set()", async () => {
await createTempDirectory();
// Write out a file.
const fileName = "file1";
const fileText = "This is the first file.";
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set(fileName, fileText);
// Read it back in.
const filePath = path.join(tempDirectory, fileName);
const actualText = String(await fs.readFile(filePath));
assert.equal(fileText, actualText);
await removeTempDirectory();
});
test("create subfolder via set() with empty object value", async () => {
await createTempDirectory();
// Write out new, empty folder called "empty".
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set("empty", {});
// Verify folder exists and has no contents.
const folderPath = path.join(tempDirectory, "empty");
const stats = await fs.stat(folderPath);
assert(stats.isDirectory());
const files = await fs.readdir(folderPath);
assert.deepEqual(files, []);
await removeTempDirectory();
});
test("create subfolder via set() with empty tree value", async () => {
await createTempDirectory();
// Write out new, empty folder called "empty".
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set("empty", new ObjectTree({}));
// Verify folder exists and has no contents.
const folderPath = path.join(tempDirectory, "empty");
const stats = await fs.stat(folderPath);
assert(stats.isDirectory());
const files = await fs.readdir(folderPath);
assert.deepEqual(files, []);
await removeTempDirectory();
});
test("can write out subfolder via set()", async () => {
await createTempDirectory();
// Create a tiny set of "files".
const obj = {
file1: "This is the first file.",
subfolder: {
file2: "This is the second file.",
},
};
// Write out files as a new folder called "folder".
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set("folder", obj);
// Read them back in.
const actualFiles = await tempFiles.get("folder");
const strings = Tree.map(actualFiles, {
deep: true,
value: (buffer) => textDecoder.decode(buffer),
});
const plain = await Tree.plain(strings);
assert.deepEqual(plain, obj);
await removeTempDirectory();
});
test("can delete a file via set()", async () => {
await createTempDirectory();
const tempFile = path.join(tempDirectory, "file");
await fs.writeFile(tempFile, "");
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set("file", undefined);
let stats;
try {
stats = await fs.stat(tempFile);
} catch (/** @type {any} */ error) {
if (error.code !== "ENOENT") {
throw error;
}
}
assert(stats === undefined);
await removeTempDirectory();
});
test("can delete a folder via set()", async () => {
await createTempDirectory();
const folder = path.join(tempDirectory, "folder");
await fs.mkdir(folder);
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set("folder", undefined);
let stats;
try {
stats = await fs.stat(folder);
} catch (/** @type {any} */ error) {
if (error.code !== "ENOENT") {
throw error;
}
}
assert(stats === undefined);
await removeTempDirectory();
});
});
function createFixture(fixturePath) {
return new FileTree(path.join(dirname, fixturePath));
}
async function createTempDirectory() {
await fs.mkdir(tempDirectory, { recursive: true });
}
async function removeTempDirectory() {
await fs.rm(tempDirectory, { recursive: true });
}

View File

@@ -0,0 +1,46 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import FunctionTree from "../../src/drivers/FunctionTree.js";
describe("FunctionTree", async () => {
test("can get the keys of the tree", async () => {
const fixture = createFixture();
assert.deepEqual(Array.from(await fixture.keys()), [
"Alice.md",
"Bob.md",
"Carol.md",
]);
});
test("can get the value for a key", async () => {
const fixture = createFixture();
const alice = await fixture.get("Alice.md");
assert.equal(alice, "Hello, **Alice**.");
});
test("getting a value from function with multiple arguments curries the function", async () => {
const fixture = new FunctionTree((a, b, c) => a + b + c);
const fnA = await fixture.get(1);
const fnAB = await fnA.get(2);
const result = await fnAB.get(3);
assert.equal(result, 6);
});
test("getting an unsupported key returns undefined", async () => {
const fixture = createFixture();
assert.equal(await fixture.get("xyz"), undefined);
});
});
function createFixture() {
return new FunctionTree(
(key) => {
if (key?.endsWith?.(".md")) {
const name = key.slice(0, -3);
return `Hello, **${name}**.`;
}
return undefined;
},
["Alice.md", "Bob.md", "Carol.md"]
);
}

View File

@@ -0,0 +1,59 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import MapTree from "../../src/drivers/MapTree.js";
import * as symbols from "../../src/symbols.js";
describe("MapTree", () => {
test("can get the keys of the tree", async () => {
const fixture = createFixture();
assert.deepEqual(Array.from(await fixture.keys()), ["a", "b", "c"]);
});
test("can get the value for a key", async () => {
const fixture = createFixture();
const a = await fixture.get("a");
assert.equal(a, 1);
});
test("sets parent on subtrees", async () => {
const map = new Map([["more", new Map([["a", 1]])]]);
const fixture = new MapTree(map);
const more = await fixture.get("more");
assert.equal(more[symbols.parent], fixture);
});
test("adds trailing slashes to keys for subtrees", async () => {
const tree = new MapTree([
["a", 1],
["subtree", new MapTree([["b", 2]])],
]);
const keys = Array.from(await tree.keys());
assert.deepEqual(keys, ["a", "subtree/"]);
});
test("can retrieve values with optional trailing slash", async () => {
const subtree = new MapTree([["b", 2]]);
const tree = new MapTree([
["a", 1],
["subtree", subtree],
]);
assert.equal(await tree.get("a"), 1);
assert.equal(await tree.get("a/"), 1);
assert.equal(await tree.get("subtree"), subtree);
assert.equal(await tree.get("subtree/"), subtree);
});
test("getting an unsupported key returns undefined", async () => {
const fixture = createFixture();
assert.equal(await fixture.get("d"), undefined);
});
});
function createFixture() {
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
return new MapTree(map);
}

View File

@@ -0,0 +1,156 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { ObjectTree, Tree } from "../../src/internal.js";
import * as symbols from "../../src/symbols.js";
describe("ObjectTree", () => {
test("can get the keys of the tree", async () => {
const fixture = createFixture();
assert.deepEqual(Array.from(await fixture.keys()), [
"Alice.md",
"Bob.md",
"Carol.md",
]);
});
test("can get the value for a key", async () => {
const fixture = createFixture();
const alice = await fixture.get("Alice.md");
assert.equal(alice, "Hello, **Alice**.");
});
test("getting an unsupported key returns undefined", async () => {
const fixture = createFixture();
assert.equal(await fixture.get("xyz"), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const fixture = createFixture();
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
test("can set a value", async () => {
const tree = new ObjectTree({
a: 1,
b: 2,
c: 3,
});
// Update existing key
await tree.set("a", 4);
// Delete key
await tree.set("b", undefined);
// Overwrite key with trailing slash
await tree.set("c/", {});
// New key
await tree.set("d", 5);
assert.deepEqual(await Tree.entries(tree), [
["a", 4],
["c/", {}],
["d", 5],
]);
});
test("can wrap a class instance", async () => {
class Foo {
constructor() {
this.a = 1;
}
get prop() {
return this._prop;
}
set prop(prop) {
this._prop = prop;
}
}
class Bar extends Foo {
method() {}
}
const bar = new Bar();
/** @type {any} */ (bar).extra = "Hello";
const fixture = new ObjectTree(bar);
assert.deepEqual(await Tree.entries(fixture), [
["a", 1],
["extra", "Hello"],
["prop", undefined],
]);
assert.equal(await fixture.get("a"), 1);
await fixture.set("prop", "Goodbye");
assert.equal(bar.prop, "Goodbye");
assert.equal(await fixture.get("prop"), "Goodbye");
});
test("sets parent symbol on subobjects", async () => {
const fixture = new ObjectTree({
sub: {},
});
const sub = await fixture.get("sub");
assert.equal(sub[symbols.parent], fixture);
});
test("sets parent on subtrees", async () => {
const fixture = new ObjectTree({
a: 1,
more: new ObjectTree({
b: 2,
}),
});
const more = await fixture.get("more");
assert.equal(more.parent, fixture);
});
test("adds trailing slashes to keys for subtrees", async () => {
const tree = new ObjectTree({
a1: 1,
a2: new ObjectTree({
b1: 2,
}),
a3: 3,
a4: new ObjectTree({
b2: 4,
}),
});
const keys = Array.from(await tree.keys());
assert.deepEqual(keys, ["a1", "a2/", "a3", "a4/"]);
});
test("can retrieve values with optional trailing slash", async () => {
const subtree = {
async get(key) {},
async keys() {},
};
const tree = new ObjectTree({
a: 1,
subtree,
});
assert.equal(await tree.get("a"), 1);
assert.equal(await tree.get("a/"), 1);
assert.equal(await tree.get("subtree"), subtree);
assert.equal(await tree.get("subtree/"), subtree);
});
test("method on an object is bound to the object", async () => {
const n = new Number(123);
const tree = new ObjectTree(n);
const method = await tree.get("toString");
assert.equal(method(), "123");
});
});
function createFixture() {
return new ObjectTree({
"Alice.md": "Hello, **Alice**.",
"Bob.md": "Hello, **Bob**.",
"Carol.md": "Hello, **Carol**.",
});
}

View File

@@ -0,0 +1,44 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import SetTree from "../../src/drivers/SetTree.js";
import { ObjectTree } from "../../src/internal.js";
describe("SetTree", () => {
test("can get the keys of the tree", async () => {
const set = new Set(["a", "b", "c"]);
const fixture = new SetTree(set);
assert.deepEqual(Array.from(await fixture.keys()), [0, 1, 2]);
});
test("can get the value for a key", async () => {
const set = new Set(["a", "b", "c"]);
const fixture = new SetTree(set);
const a = await fixture.get(0);
assert.equal(a, "a");
});
test("getting an unsupported key returns undefined", async () => {
const set = new Set(["a", "b", "c"]);
const fixture = new SetTree(set);
assert.equal(await fixture.get(3), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const set = new Set(["a", "b", "c"]);
const fixture = new SetTree(set);
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
test("sets parent on subtrees", async () => {
const set = new Set();
set.add(new ObjectTree({}));
const fixture = new SetTree(set);
const subtree = await fixture.get(0);
assert.equal(subtree.parent, fixture);
});
});

View File

@@ -0,0 +1,92 @@
import assert from "node:assert";
import { beforeEach, describe, mock, test } from "node:test";
import SiteTree from "../../src/drivers/SiteTree.js";
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
const mockHost = "https://mock";
const mockResponses = {
"/about": {
redirected: true,
status: 301,
url: "https://mock/about/",
},
"/about/Alice.html": {
data: "Hello, Alice!",
},
"/about/Bob.html": {
data: "Hello, Bob!",
},
"/about/Carol.html": {
data: "Hello, Carol!",
},
"/index.html": {
data: "Home page",
},
};
describe("SiteTree", () => {
beforeEach(() => {
mock.method(global, "fetch", mockFetch);
});
test("returns an empty array as the keys of a tree", async () => {
const fixture = new SiteTree(mockHost);
const keys = await fixture.keys();
assert.deepEqual(Array.from(keys), []);
});
test("can get a plain value for a key", async () => {
const fixture = new SiteTree(mockHost);
const arrayBuffer = await fixture.get("index.html");
const text = textDecoder.decode(arrayBuffer);
assert.equal(text, "Home page");
});
test("immediately return a new tree for a key with a trailing slash", async () => {
const fixture = new SiteTree(mockHost);
const about = await fixture.get("about/");
assert(about instanceof SiteTree);
assert.equal(about.href, "https://mock/about/");
});
test("getting an unsupported key returns undefined", async () => {
const fixture = new SiteTree(mockHost);
assert.equal(await fixture.get("xyz"), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const fixture = new SiteTree(mockHost);
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
});
async function mockFetch(href) {
if (!href.startsWith(mockHost)) {
return { status: 404 };
}
const path = href.slice(mockHost.length);
const mockedResponse = mockResponses[path];
if (mockedResponse) {
return Object.assign(
{
arrayBuffer: () => textEncoder.encode(mockedResponse.data).buffer,
ok: true,
status: 200,
text: () => mockedResponse.data,
},
mockedResponse
);
}
return {
ok: false,
status: 404,
};
}

View File

@@ -0,0 +1,121 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import calendar from "../../src/drivers/calendarTree.js";
import { toPlainValue } from "../../src/utilities.js";
describe("calendarTree", () => {
test("without a start or end, returns a tree for today", async () => {
const tree = calendar({
value: (year, month, day) => `${year}-${month}-${day}`,
});
const plain = await toPlainValue(tree);
const today = new Date();
const year = today.getFullYear();
const month = (today.getMonth() + 1).toString().padStart(2, "0");
const day = today.getDate().toString().padStart(2, "0");
assert.deepEqual(plain, {
[year]: {
[month]: {
[day]: `${year}-${month}-${day}`,
},
},
});
});
test("returns a tree for a month range", async () => {
const tree = calendar({
start: "2025-01",
end: "2025-02",
value: (year, month, day) => `${year}-${month}-${day}`,
});
const plain = await toPlainValue(tree);
assert.deepEqual(plain, {
2025: {
"01": {
"01": "2025-01-01",
"02": "2025-01-02",
"03": "2025-01-03",
"04": "2025-01-04",
"05": "2025-01-05",
"06": "2025-01-06",
"07": "2025-01-07",
"08": "2025-01-08",
"09": "2025-01-09",
10: "2025-01-10",
11: "2025-01-11",
12: "2025-01-12",
13: "2025-01-13",
14: "2025-01-14",
15: "2025-01-15",
16: "2025-01-16",
17: "2025-01-17",
18: "2025-01-18",
19: "2025-01-19",
20: "2025-01-20",
21: "2025-01-21",
22: "2025-01-22",
23: "2025-01-23",
24: "2025-01-24",
25: "2025-01-25",
26: "2025-01-26",
27: "2025-01-27",
28: "2025-01-28",
29: "2025-01-29",
30: "2025-01-30",
31: "2025-01-31",
},
"02": {
"01": "2025-02-01",
"02": "2025-02-02",
"03": "2025-02-03",
"04": "2025-02-04",
"05": "2025-02-05",
"06": "2025-02-06",
"07": "2025-02-07",
"08": "2025-02-08",
"09": "2025-02-09",
10: "2025-02-10",
11: "2025-02-11",
12: "2025-02-12",
13: "2025-02-13",
14: "2025-02-14",
15: "2025-02-15",
16: "2025-02-16",
17: "2025-02-17",
18: "2025-02-18",
19: "2025-02-19",
20: "2025-02-20",
21: "2025-02-21",
22: "2025-02-22",
23: "2025-02-23",
24: "2025-02-24",
25: "2025-02-25",
26: "2025-02-26",
27: "2025-02-27",
28: "2025-02-28",
},
},
});
});
test("returns a tree for a day range", async () => {
const tree = calendar({
start: "2025-02-27",
end: "2025-03-02",
value: (year, month, day) => `${year}-${month}-${day}`,
});
const plain = await toPlainValue(tree);
assert.deepEqual(plain, {
2025: {
"02": {
27: "2025-02-27",
28: "2025-02-28",
},
"03": {
"01": "2025-03-01",
"02": "2025-03-02",
},
},
});
});
});

View File

@@ -0,0 +1 @@
Hello, **Alice**.

View File

@@ -0,0 +1 @@
Hello, **Bob**.

View File

@@ -0,0 +1 @@
Hello, **Carol**.

View File

@@ -0,0 +1 @@
This file exists to force the creation of the parent folder.

View File

@@ -0,0 +1,41 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { extname, match, replace } from "../src/extension.js";
describe("extension", () => {
test("extname", () => {
assert.equal(extname(".\\"), "");
assert.equal(extname("..\\"), ".\\");
assert.equal(extname("file.ext\\"), ".ext\\");
assert.equal(extname("file.ext\\\\"), ".ext\\\\");
assert.equal(extname("file\\"), "");
assert.equal(extname("file\\\\"), "");
assert.equal(extname("file.\\"), ".\\");
assert.equal(extname("file.\\\\"), ".\\\\");
});
test("match", () => {
assert.equal(match("file.md", ".md"), "file");
assert.equal(match("file.md", ".txt"), null);
assert.equal(match("file.md/", ".md"), "file/");
assert.equal(match("file", ""), "file");
assert.equal(match("file", "/"), null);
assert.equal(match("file/", "/"), "file");
});
test("match can handle multi-part extensions", () => {
assert.equal(match("foo.ori.html", ".ori.html"), "foo");
assert.equal(match("foo.ori.html", ".html"), "foo.ori");
assert.equal(match("foo.ori.html", ".txt"), null);
assert.equal(match("foo.ori.html/", ".ori.html"), "foo/");
});
test("replace", () => {
assert.equal(replace("file.md", ".md", ".html"), "file.html");
assert.equal(replace("file.md", ".txt", ".html"), "file.md");
assert.equal(replace("file.md/", ".md", ".html"), "file.html/");
assert.equal(replace("folder/", "", ".html"), "folder.html");
assert.equal(replace("folder", "/", ".html"), "folder");
assert.equal(replace("folder/", "/", ".html"), "folder.html");
});
});

View File

@@ -0,0 +1,15 @@
import { DeepObjectTree } from "@weborigami/async-tree";
import assert from "node:assert";
import { describe, test } from "node:test";
import * as jsonKeys from "../src/jsonKeys.js";
describe("jsonKeys", () => {
test("stringifies JSON Keys", async () => {
const tree = new DeepObjectTree({
about: {},
"index.html": "Home",
});
const json = await jsonKeys.stringify(tree);
assert.strictEqual(json, '["about/","index.html"]');
});
});

View File

@@ -0,0 +1,63 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, ObjectTree, Tree } from "../../src/internal.js";
import cache from "../../src/operations/cache.js";
describe("cache", () => {
test("caches reads of values from one tree into another", async () => {
const objectCache = new ObjectTree({});
const fixture = cache(
new DeepObjectTree({
a: 1,
b: 2,
c: 3,
more: {
d: 4,
},
}),
objectCache
);
const keys = [...(await fixture.keys())];
assert.deepEqual(keys, ["a", "b", "c", "more/"]);
assert.equal(await objectCache.get("a"), undefined);
assert.equal(await fixture.get("a"), 1);
assert.equal(await objectCache.get("a"), 1); // Now in cache
assert.equal(await objectCache.get("b"), undefined);
assert.equal(await fixture.get("b"), 2);
assert.equal(await objectCache.get("b"), 2);
assert.equal(await objectCache.get("more"), undefined);
const more = await fixture.get("more");
assert.deepEqual([...(await more.keys())], ["d"]);
assert.equal(await more.get("d"), 4);
const moreCache = await objectCache.get("more");
assert.equal(await moreCache.get("d"), 4);
});
test("if a cache filter is supplied, it only caches values whose keys match the filter", async () => {
const objectCache = new ObjectTree({});
const fixture = cache(
Tree.from({
"a.txt": "a",
"b.txt": "b",
}),
objectCache,
Tree.from({
"a.txt": true,
})
);
// Access some values to populate the cache.
assert.equal(await fixture.get("a.txt"), "a");
assert.equal(await fixture.get("b.txt"), "b");
// The a.txt value should be cached because it matches the filter.
assert.equal(await objectCache.get("a.txt"), "a");
// The b.txt value should not be cached because it does not match the filter.
assert.equal(await objectCache.get("b.txt"), undefined);
});
});

View File

@@ -0,0 +1,90 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, ObjectTree } from "../../src/internal.js";
import cachedKeyFunctions from "../../src/operations/cachedKeyFunctions.js";
describe("cachedKeyFunctions", () => {
test("maps keys with caching", async () => {
const tree = new ObjectTree({
a: "letter a",
b: "letter b",
});
let callCount = 0;
const addUnderscore = async (sourceKey, tree) => {
callCount++;
return `_${sourceKey}`;
};
const { inverseKey, key } = cachedKeyFunctions(addUnderscore);
assert.equal(await inverseKey("_a", tree), "a"); // Cache miss
assert.equal(callCount, 1);
assert.equal(await inverseKey("_a", tree), "a");
assert.equal(callCount, 1);
assert.equal(await inverseKey("_b", tree), "b"); // Cache miss
assert.equal(callCount, 2);
assert.equal(await key("a", tree), "_a");
assert.equal(await key("a", tree), "_a");
assert.equal(await key("b", tree), "_b");
assert.equal(callCount, 2);
// `c` isn't in tree, so we should get undefined.
assert.equal(await inverseKey("_c", tree), undefined);
// But key mapping is still possible.
assert.equal(await key("c", tree), "_c");
// And now we have a cache hit.
assert.equal(await inverseKey("_c", tree), "c");
assert.equal(callCount, 3);
});
test("maps keys with caching and deep option", async () => {
const tree = new DeepObjectTree({
a: "letter a",
b: {
c: "letter c",
},
});
let callCount = 0;
const addUnderscore = async (sourceKey, tree) => {
callCount++;
return `_${sourceKey}`;
};
const { inverseKey, key } = cachedKeyFunctions(addUnderscore, true);
assert.equal(await inverseKey("_a", tree), "a"); // Cache miss
assert.equal(await inverseKey("_a", tree), "a");
assert.equal(callCount, 1);
// Subtree key left alone
assert.equal(await inverseKey("_b", tree), undefined);
assert.equal(await inverseKey("b", tree), "b");
assert.equal(await inverseKey("b/", tree), "b/");
assert.equal(callCount, 1);
assert.equal(await key("a", tree), "_a");
assert.equal(await key("a", tree), "_a");
assert.equal(callCount, 1);
assert.equal(await key("b/", tree), "b/");
assert.equal(await key("b", tree), "b");
assert.equal(callCount, 1);
});
test("preserves trailing slashes", async () => {
const tree = new ObjectTree({
a: "letter a",
});
const addUnderscore = async (sourceKey) => `_${sourceKey}`;
const { inverseKey, key } = cachedKeyFunctions(addUnderscore);
assert.equal(await key("a/", tree), "_a/");
assert.equal(await key("a", tree), "_a");
assert.equal(await inverseKey("_a/", tree), "a/");
assert.equal(await inverseKey("_a", tree), "a");
});
});

View File

@@ -0,0 +1,34 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import FunctionTree from "../../src/drivers/FunctionTree.js";
import { Tree } from "../../src/internal.js";
import concat from "../../src/operations/concat.js";
describe("concat", () => {
test("concatenates deep tree values", async () => {
const tree = Tree.from({
a: "A",
b: "B",
c: "C",
more: {
d: "D",
e: "E",
},
});
const result = await concat.call(null, tree);
assert.equal(result, "ABCDE");
});
test("concatenates deep tree-like values", async () => {
const letters = ["a", "b", "c"];
const specimens = new FunctionTree(
(letter) => ({
lowercase: letter,
uppercase: letter.toUpperCase(),
}),
letters
);
const result = await concat.call(null, specimens);
assert.equal(result, "aAbBcC");
});
});

View File

@@ -0,0 +1,42 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, Tree } from "../../src/internal.js";
import mergeDeep from "../../src/operations/deepMerge.js";
describe("mergeDeep", () => {
test("can merge deep", async () => {
const fixture = mergeDeep(
new DeepObjectTree({
a: {
b: 0, // Will be obscured by `b` below
c: {
d: 2,
},
},
}),
new DeepObjectTree({
a: {
b: 1,
c: {
e: 3,
},
f: 4,
},
})
);
assert.deepEqual(await Tree.plain(fixture), {
a: {
b: 1,
c: {
d: 2,
e: 3,
},
f: 4,
},
});
// Parent of a subvalue is the merged tree
const a = await fixture.get("a");
assert.equal(a.parent, fixture);
});
});

View File

@@ -0,0 +1,24 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import deepReverse from "../../src/operations/deepReverse.js";
describe("deepReverse", () => {
test("reverses keys at all levels of a tree", async () => {
const tree = {
a: 1,
b: {
c: 2,
d: 3,
},
};
const reversed = deepReverse.call(null, tree);
assert.deepEqual(await Tree.plain(reversed), {
b: {
d: 3,
c: 2,
},
a: 1,
});
});
});

View File

@@ -0,0 +1,22 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import deepTake from "../../src/operations/deepTake.js";
describe("deepTake", () => {
test("traverses deeply and returns a limited number of items", async () => {
const tree = {
a: 1,
b: {
c: 2,
d: {
e: 3,
},
f: 4,
},
g: 5,
};
const result = await deepTake(tree, 4);
assert.deepEqual(await Tree.plain(result), [1, 2, 3, 4]);
});
});

View File

@@ -0,0 +1,28 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import deepValues from "../../src/operations/deepValues.js";
describe("deepValues", () => {
test("returns in-order array of a tree's values", async () => {
const tree = {
a: 1,
b: {
c: 2,
d: {
e: 3,
},
},
f: {
g: 4,
},
};
const values = await deepValues(tree);
assert.deepEqual(values, [1, 2, 3, 4]);
});
test("returns in-order array of values in nested arrays", async () => {
const tree = [1, [2, 3], 4];
const values = await deepValues(tree);
assert.deepEqual(values, [1, 2, 3, 4]);
});
});

View File

@@ -0,0 +1,23 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { ObjectTree } from "../../src/internal.js";
import deepValuesIterator from "../../src/operations/deepValuesIterator.js";
describe("deepValuesIterator", () => {
test("returns an iterator of a tree's deep values", async () => {
const tree = new ObjectTree({
a: 1,
b: 2,
more: {
c: 3,
d: 4,
},
});
const values = [];
// The tree will be shallow, but we'll ask to expand the values.
for await (const value of deepValuesIterator(tree, { expand: true })) {
values.push(value);
}
assert.deepEqual(values, [1, 2, 3, 4]);
});
});

View File

@@ -0,0 +1,54 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import group from "../../src/operations/group.js";
describe("group transform", () => {
test("groups an array using a group key function", async () => {
const fonts = [
{ name: "Aboreto", tags: ["Sans Serif"] },
{ name: "Albert Sans", tags: ["Geometric", "Sans Serif"] },
{ name: "Alegreya", tags: ["Serif"] },
{ name: "Work Sans", tags: ["Grotesque", "Sans Serif"] },
];
const tree = Tree.from(fonts);
const grouped = await group(tree, (value, key, tree) => value.tags);
assert.deepEqual(await Tree.plain(grouped), {
Geometric: [{ name: "Albert Sans", tags: ["Geometric", "Sans Serif"] }],
Grotesque: [{ name: "Work Sans", tags: ["Grotesque", "Sans Serif"] }],
"Sans Serif": [
{ name: "Aboreto", tags: ["Sans Serif"] },
{ name: "Albert Sans", tags: ["Geometric", "Sans Serif"] },
{ name: "Work Sans", tags: ["Grotesque", "Sans Serif"] },
],
Serif: [{ name: "Alegreya", tags: ["Serif"] }],
});
});
test("groups an object using a group key function", async () => {
const fonts = {
Aboreto: { tags: ["Sans Serif"] },
"Albert Sans": { tags: ["Geometric", "Sans Serif"] },
Alegreya: { tags: ["Serif"] },
"Work Sans": { tags: ["Grotesque", "Sans Serif"] },
};
const tree = Tree.from(fonts);
const grouped = await group(tree, (value, key, tree) => value.tags);
assert.deepEqual(await Tree.plain(grouped), {
Geometric: {
"Albert Sans": { tags: ["Geometric", "Sans Serif"] },
},
Grotesque: {
"Work Sans": { tags: ["Grotesque", "Sans Serif"] },
},
"Sans Serif": {
Aboreto: { tags: ["Sans Serif"] },
"Albert Sans": { tags: ["Geometric", "Sans Serif"] },
"Work Sans": { tags: ["Grotesque", "Sans Serif"] },
},
Serif: {
Alegreya: { tags: ["Serif"] },
},
});
});
});

View File

@@ -0,0 +1,17 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import invokeFunctions from "../../src/operations/invokeFunctions.js";
describe("invokeFunctions", () => {
test("invokes function values, leaves other values as is", async () => {
const fixture = invokeFunctions({
a: 1,
b: () => 2,
});
assert.deepEqual(await Tree.plain(fixture), {
a: 1,
b: 2,
});
});
});

View File

@@ -0,0 +1,82 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { ObjectTree, Tree } from "../../src/internal.js";
import keyFunctionsForExtensions from "../../src/operations/keyFunctionsForExtensions.js";
import map from "../../src/operations/map.js";
describe("keyMapsForExtensions", () => {
test("returns key functions that pass a matching key through", async () => {
const { inverseKey, key } = keyFunctionsForExtensions({
sourceExtension: ".txt",
});
assert.equal(await inverseKey("file.txt"), "file.txt");
assert.equal(await inverseKey("file.txt/"), "file.txt");
assert.equal(await key("file.txt"), "file.txt");
assert.equal(await key("file.txt/"), "file.txt/");
assert.equal(await inverseKey("file.foo"), undefined);
assert.equal(await key("file.foo"), undefined);
});
test("returns key functions that can map extensions", async () => {
const { inverseKey, key } = keyFunctionsForExtensions({
resultExtension: ".json",
sourceExtension: ".md",
});
assert.equal(await inverseKey("file.json"), "file.md");
assert.equal(await inverseKey("file.json/"), "file.md");
assert.equal(await key("file.md"), "file.json");
assert.equal(await key("file.md/"), "file.json/");
assert.equal(await inverseKey("file.foo"), undefined);
assert.equal(await key("file.foo"), undefined);
});
test("key functions can handle a slash as an explicit extension", async () => {
const { inverseKey, key } = keyFunctionsForExtensions({
resultExtension: ".html",
sourceExtension: "/",
});
assert.equal(await inverseKey("file.html"), "file/");
assert.equal(await inverseKey("file.html/"), "file/");
assert.equal(await key("file"), undefined);
assert.equal(await key("file/"), "file.html");
});
test("works with map to handle keys that end in a given resultExtension", async () => {
const files = new ObjectTree({
"file1.txt": "will be mapped",
file2: "won't be mapped",
"file3.foo": "won't be mapped",
});
const { inverseKey, key } = keyFunctionsForExtensions({
sourceExtension: ".txt",
});
const fixture = map(files, {
inverseKey,
key,
value: (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
});
assert.deepEqual(await Tree.plain(fixture), {
"file1.txt": "WILL BE MAPPED",
});
});
test("works with map to change a key's resultExtension", async () => {
const files = new ObjectTree({
"file1.txt": "will be mapped",
file2: "won't be mapped",
"file3.foo": "won't be mapped",
});
const { inverseKey, key } = keyFunctionsForExtensions({
resultExtension: ".upper",
sourceExtension: ".txt",
});
const fixture = map(files, {
inverseKey,
key,
value: (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
});
assert.deepEqual(await Tree.plain(fixture), {
"file1.upper": "WILL BE MAPPED",
});
});
});

View File

@@ -0,0 +1,204 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import FunctionTree from "../../src/drivers/FunctionTree.js";
import { DeepObjectTree, ObjectTree, Tree } from "../../src/internal.js";
import map from "../../src/operations/map.js";
import * as trailingSlash from "../../src/trailingSlash.js";
describe("map", () => {
test("returns identity graph if no key or value function is supplied", async () => {
const tree = {
a: "letter a",
b: "letter b",
};
const mapped = map(tree, {});
assert.deepEqual(await Tree.plain(mapped), {
a: "letter a",
b: "letter b",
});
});
test("maps values", async () => {
const tree = new ObjectTree({
a: "letter a",
b: "letter b",
c: undefined, // Won't be mapped
});
const mapped = map(tree, {
value: (sourceValue, sourceKey, innerTree) => {
assert(sourceKey === "a" || sourceKey === "b");
assert.equal(innerTree, tree);
return sourceValue.toUpperCase();
},
});
assert.deepEqual(await Tree.plain(mapped), {
a: "LETTER A",
b: "LETTER B",
c: undefined,
});
});
test("interprets a single function argument as the value function", async () => {
const tree = {
a: "letter a",
b: "letter b",
};
const uppercaseValues = map(tree, (sourceValue, sourceKey, tree) => {
assert(sourceKey === "a" || sourceKey === "b");
return sourceValue.toUpperCase();
});
assert.deepEqual(await Tree.plain(uppercaseValues), {
a: "LETTER A",
b: "LETTER B",
});
});
test("maps keys using key and inverseKey", async () => {
const tree = {
a: "letter a",
b: "letter b",
};
const underscoreKeys = map(tree, {
key: addUnderscore,
inverseKey: removeUnderscore,
});
assert.deepEqual(await Tree.plain(underscoreKeys), {
_a: "letter a",
_b: "letter b",
});
});
test("maps keys and values", async () => {
const tree = {
a: "letter a",
b: "letter b",
};
const underscoreKeysUppercaseValues = map(tree, {
key: addUnderscore,
inverseKey: removeUnderscore,
value: async (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
});
assert.deepEqual(await Tree.plain(underscoreKeysUppercaseValues), {
_a: "LETTER A",
_b: "LETTER B",
});
});
test("a shallow map is applied to async subtrees too", async () => {
const tree = {
a: "letter a",
more: {
b: "letter b",
},
};
const underscoreKeys = map(tree, {
key: async (sourceKey, tree) => `_${sourceKey}`,
inverseKey: async (resultKey, tree) => resultKey.slice(1),
value: async (sourceValue, sourceKey, tree) => sourceKey,
});
assert.deepEqual(await Tree.plain(underscoreKeys), {
_a: "a",
_more: "more",
});
});
test("value can provide a default key and inverse key functions", async () => {
const uppercase = (s) => s.toUpperCase();
uppercase.key = addUnderscore;
uppercase.inverseKey = removeUnderscore;
const tree = {
a: "letter a",
b: "letter b",
};
const mapped = map(tree, uppercase);
assert.deepEqual(await Tree.plain(mapped), {
_a: "LETTER A",
_b: "LETTER B",
});
});
test("deep maps values", async () => {
const tree = new DeepObjectTree({
a: "letter a",
more: {
b: "letter b",
},
});
const uppercaseValues = map(tree, {
deep: true,
value: (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
});
assert.deepEqual(await Tree.plain(uppercaseValues), {
a: "LETTER A",
more: {
b: "LETTER B",
},
});
});
test("deep maps leaf keys", async () => {
const tree = new DeepObjectTree({
a: "letter a",
more: {
b: "letter b",
},
});
const underscoreKeys = map(tree, {
deep: true,
key: addUnderscore,
inverseKey: removeUnderscore,
});
assert.deepEqual(await Tree.plain(underscoreKeys), {
_a: "letter a",
more: {
_b: "letter b",
},
});
});
test("deep maps leaf keys and values", async () => {
const tree = new DeepObjectTree({
a: "letter a",
more: {
b: "letter b",
},
});
const underscoreKeysUppercaseValues = map(tree, {
deep: true,
key: addUnderscore,
inverseKey: removeUnderscore,
value: async (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
});
assert.deepEqual(await Tree.plain(underscoreKeysUppercaseValues), {
_a: "LETTER A",
more: {
_b: "LETTER B",
},
});
});
test("needsSourceValue can be set to false in cases where the value isn't necessary", async () => {
let flag = false;
const tree = new FunctionTree(() => {
flag = true;
}, ["a", "b", "c"]);
const mapped = map(tree, {
needsSourceValue: false,
value: () => "X",
});
assert.deepEqual(await Tree.plain(mapped), {
a: "X",
b: "X",
c: "X",
});
assert(!flag);
});
});
function addUnderscore(key) {
return `_${key}`;
}
function removeUnderscore(key) {
return trailingSlash.has(key) ? key : key.slice(1);
}

View File

@@ -0,0 +1,64 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, ObjectTree, Tree } from "../../src/internal.js";
import merge from "../../src/operations/merge.js";
import * as symbols from "../../src/symbols.js";
describe("merge", () => {
test("performs a shallow merge", async () => {
const fixture = merge(
{
a: 1,
// Will be obscured by `b` that follows
b: {
c: 2,
},
},
{
b: {
d: 3,
},
e: {
f: 4,
},
}
);
assert.deepEqual(await Tree.plain(fixture), {
a: 1,
b: {
d: 3,
},
e: {
f: 4,
},
});
// Merge is shallow, and last tree wins, so `b/c` doesn't exist
const c = await Tree.traverse(fixture, "b", "c");
assert.equal(c, undefined);
// Parent of a subvalue is the merged tree
const b = await fixture.get("b");
assert.equal(b[symbols.parent], fixture);
});
test("subtree can overwrite a leaf node", async () => {
const fixture = merge(
new ObjectTree({
a: 1,
}),
new DeepObjectTree({
a: {
b: 2,
},
})
);
assert.deepEqual([...(await fixture.keys())], ["a/"]);
assert.deepEqual(await Tree.plain(fixture), {
a: {
b: 2,
},
});
});
});

View File

@@ -0,0 +1,25 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, Tree } from "../../src/internal.js";
import regExpKeys from "../../src/operations/regExpKeys.js";
describe("regExpKeys", () => {
test("matches keys using regular expressions", async () => {
const fixture = await regExpKeys(
new DeepObjectTree({
a: true,
"b.*": true,
c: {
d: true,
"e*": true,
},
})
);
assert(await Tree.traverse(fixture, "a"));
assert(!(await Tree.traverse(fixture, "alice")));
assert(await Tree.traverse(fixture, "bob"));
assert(await Tree.traverse(fixture, "brenda"));
assert(await Tree.traverse(fixture, "c", "d"));
assert(await Tree.traverse(fixture, "c", "eee"));
});
});

View File

@@ -0,0 +1,23 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import reverse from "../../src/operations/reverse.js";
describe("reverse", () => {
test("reverses a tree's top-level keys", async () => {
const tree = {
a: "A",
b: "B",
c: "C",
};
const reversed = reverse.call(null, tree);
// @ts-ignore
assert.deepEqual(Array.from(await reversed.keys()), ["c", "b", "a"]);
// @ts-ignore
assert.deepEqual(await Tree.plain(reversed), {
c: "C",
b: "B",
a: "A",
});
});
});

View File

@@ -0,0 +1,25 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { ObjectTree } from "../../src/internal.js";
import scope from "../../src/operations/scope.js";
describe("scope", () => {
test("gets the first defined value from the scope trees", async () => {
const outer = new ObjectTree({
a: 1,
b: 2,
});
const inner = new ObjectTree({
a: 3,
});
inner.parent = outer;
const innerScope = scope(inner);
assert.deepEqual([...(await innerScope.keys())], ["a", "b"]);
// Inner tree has precedence
assert.equal(await innerScope.get("a"), 3);
// If tree doesn't have value, finds value from parent
assert.equal(await innerScope.get("b"), 2);
assert.equal(await innerScope.get("c"), undefined);
assert.deepEqual(innerScope.trees, [inner, outer]);
});
});

View File

@@ -0,0 +1,48 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import sort from "../../src/operations/sort.js";
describe("sort", () => {
test("sorts keys using default sort order", async () => {
const tree = Tree.from({
file10: null,
file1: null,
file9: null,
});
const sorted = sort(tree);
assert.deepEqual(Array.from(await sorted.keys()), [
"file1",
"file10",
"file9",
]);
});
test("invokes a comparison function", async () => {
const tree = Tree.from({
b: 2,
c: 3,
a: 1,
});
// Reverse order
const compare = (a, b) => (a > b ? -1 : a < b ? 1 : 0);
const sorted = sort(tree, { compare });
assert.deepEqual(Array.from(await sorted.keys()), ["c", "b", "a"]);
});
test("invokes a sortKey function", async () => {
const tree = {
Alice: { age: 48 },
Bob: { age: 36 },
Carol: { age: 42 },
};
const sorted = await sort(tree, {
sortKey: async (key, tree) => Tree.traverse(tree, key, "age"),
});
assert.deepEqual(Array.from(await sorted.keys()), [
"Bob",
"Carol",
"Alice",
]);
});
});

View File

@@ -0,0 +1,20 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import take from "../../src/operations/take.js";
describe("take", () => {
test("limits the number of keys to the indicated count", async () => {
const tree = {
a: 1,
b: 2,
c: 3,
d: 4,
};
const result = await take(tree, 2);
assert.deepEqual(await Tree.plain(result), {
a: 1,
b: 2,
});
});
});

View File

@@ -0,0 +1,36 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { add, has, remove, toggle } from "../src/trailingSlash.js";
describe("trailingSlash", () => {
test("add adds a trailing slash to a string key for a truthy value", () => {
assert.equal(add("key"), "key/");
assert.equal(add("key/"), "key/");
assert.equal(add(1), 1);
});
test("has returns true if a string key has a trailing slash", () => {
assert.equal(has("key/"), true);
assert.equal(has("key"), false);
assert.equal(has(1), false);
});
test("remove removes a trailing slash from a string key", () => {
assert.equal(remove("key/"), "key");
assert.equal(remove("key"), "key");
assert.equal(remove(1), 1);
});
test("toggle removes a slash if present, adds one if not", () => {
assert.equal(toggle("key/"), "key");
assert.equal(toggle("key"), "key/");
assert.equal(toggle(1), 1);
});
test("toggle can force toggling on or off", () => {
assert.equal(toggle("key/", false), "key");
assert.equal(toggle("key/", true), "key/");
assert.equal(toggle("key", false), "key");
assert.equal(toggle("key", true), "key/");
});
});

View File

@@ -0,0 +1,140 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { ObjectTree } from "../src/internal.js";
import * as symbols from "../src/symbols.js";
import * as utilities from "../src/utilities.js";
describe("utilities", () => {
test("box returns a boxed value", () => {
const string = "string";
const stringObject = utilities.box(string);
assert(stringObject instanceof String);
assert.equal(stringObject, string);
const number = 1;
const numberObject = utilities.box(number);
assert(numberObject instanceof Number);
assert.equal(numberObject, number);
const boolean = true;
const booleanObject = utilities.box(boolean);
assert(booleanObject instanceof Boolean);
assert.equal(booleanObject, boolean);
const object = {};
const boxedObject = utilities.box(object);
assert.equal(boxedObject, object);
});
test("getRealmObjectPrototype returns the object's root prototype", () => {
const object = {};
const proto = utilities.getRealmObjectPrototype(object);
assert.equal(proto, Object.prototype);
});
test("isPlainObject returns true if the object is a plain object", () => {
assert.equal(utilities.isPlainObject({}), true);
assert.equal(utilities.isPlainObject(new Object()), true);
assert.equal(utilities.isPlainObject(Object.create(null)), true);
class Foo {}
assert.equal(utilities.isPlainObject(new Foo()), false);
});
test("keysFromPath() returns the keys from a slash-separated path", () => {
assert.deepEqual(utilities.keysFromPath(""), []);
assert.deepEqual(utilities.keysFromPath("/"), []);
assert.deepEqual(utilities.keysFromPath("a/b/c"), ["a/", "b/", "c"]);
assert.deepEqual(utilities.keysFromPath("a/b/c/"), ["a/", "b/", "c/"]);
assert.deepEqual(utilities.keysFromPath("/foo/"), ["foo/"]);
assert.deepEqual(utilities.keysFromPath("a///b"), ["a/", "b"]);
});
test("naturalOrder compares strings in natural order", () => {
const strings = ["file10", "file1", "file9"];
strings.sort(utilities.naturalOrder);
assert.deepEqual(strings, ["file1", "file9", "file10"]);
});
test("pathFromKeys() returns a slash-separated path from keys", () => {
assert.equal(utilities.pathFromKeys([]), "");
assert.equal(utilities.pathFromKeys(["a", "b", "c"]), "a/b/c");
assert.equal(utilities.pathFromKeys(["a/", "b/", "c"]), "a/b/c");
});
test("pipeline applies a series of functions to a value", async () => {
const addOne = (n) => n + 1;
const double = (n) => n * 2;
const square = (n) => n * n;
const result = await utilities.pipeline(1, addOne, double, square);
assert.equal(result, 16);
});
test("setParent sets a child's parent", () => {
const parent = new ObjectTree({});
// Set [symbols.parent] on a plain object.
const object = {};
utilities.setParent(object, parent);
assert.equal(object[symbols.parent], parent);
// Leave [symbols.parent] alone if it's already set.
const childWithParent = {
[symbols.parent]: "parent",
};
utilities.setParent(childWithParent, parent);
assert.equal(childWithParent[symbols.parent], "parent");
// Set `parent` on a tree.
const tree = new ObjectTree({});
utilities.setParent(tree, parent);
assert.equal(tree.parent, parent);
// Leave `parent` alone if it's already set.
const treeWithParent = new ObjectTree({});
treeWithParent.parent = "parent";
utilities.setParent(treeWithParent, parent);
assert.equal(treeWithParent.parent, "parent");
});
test("toPlainValue returns the plainest representation of an object", async () => {
class User {
constructor(name) {
this.name = name;
}
}
assert.equal(await utilities.toPlainValue(1), 1);
assert.equal(await utilities.toPlainValue("string"), "string");
assert.deepEqual(await utilities.toPlainValue({ a: 1 }), { a: 1 });
assert.equal(
await utilities.toPlainValue(new TextEncoder().encode("bytes")),
"bytes"
);
// ArrayBuffer with non-printable characters should be returned as base64
assert.equal(
await utilities.toPlainValue(new Uint8Array([1, 2, 3]).buffer),
"AQID"
);
assert.equal(await utilities.toPlainValue(async () => "result"), "result");
assert.deepEqual(await utilities.toPlainValue(new User("Alice")), {
name: "Alice",
});
});
test("toString returns the value of an object's `toString` method", () => {
const object = {
toString: () => "text",
};
assert.equal(utilities.toString(object), "text");
});
test("toString returns null for an object with no useful `toString`", () => {
const object = {};
assert.equal(utilities.toString(object), null);
});
test("toString decodes an ArrayBuffer as UTF-8", () => {
const arrayBuffer = new TextEncoder().encode("text").buffer;
assert.equal(utilities.toString(arrayBuffer), "text");
});
});