diff --git a/docs/unittest/ajax-simple-sub.json b/docs/unittest/ajax-simple-sub.json
new file mode 100644
index 0000000..1cef475
--- /dev/null
+++ b/docs/unittest/ajax-simple-sub.json
@@ -0,0 +1 @@
+[{ "title": "SubNode 1" }, { "title": "SubNode 2" }]
diff --git a/docs/unittest/ajax-simple.json b/docs/unittest/ajax-simple.json
new file mode 100644
index 0000000..76f8934
--- /dev/null
+++ b/docs/unittest/ajax-simple.json
@@ -0,0 +1,8 @@
+[
+ {
+ "title": "Node 1",
+ "expanded": true,
+ "children": [{ "title": "Node 1.1" }, { "title": "Node 1.2" }]
+ },
+ { "title": "Node 2", "lazy": true }
+]
diff --git a/docs/unittest/test-core-fixture1.js b/docs/unittest/test-core-fixture1.js
new file mode 100644
index 0000000..1c14dba
--- /dev/null
+++ b/docs/unittest/test-core-fixture1.js
@@ -0,0 +1,37 @@
+/*!
+ * Wunderbaum - Unit Test
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
+ */
+
+const { test } = QUnit;
+const Wunderbaum = mar10.Wunderbaum;
+
+const FIXTURE_1 = [
+ {
+ title: "Node 1",
+ expanded: true,
+ children: [{ title: "Node 1.1" }, { title: "Node 1.2" }],
+ },
+ { title: "Node 2", lazy: true },
+];
+
+QUnit.module("Fixture-1 tests", (hooks) => {
+ let tree;
+
+ hooks.beforeEach(() => {
+ tree = new Wunderbaum({
+ element: "#tree",
+ source: FIXTURE_1,
+ });
+ });
+ hooks.afterEach(() => {
+ tree.destroy();
+ tree = null;
+ });
+
+ test("fixture ok", (assert) => {
+ assert.expect(1);
+ assert.equal(tree.count(), 4);
+ });
+});
diff --git a/docs/unittest/test-core.js b/docs/unittest/test-core.js
new file mode 100644
index 0000000..cf8b6fe
--- /dev/null
+++ b/docs/unittest/test-core.js
@@ -0,0 +1,225 @@
+/*!
+ * Wunderbaum - Unit Test
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
+ */
+/* global mar10, QUnit */
+/* eslint-env browser */
+/* eslint-disable no-console */
+
+const { test } = QUnit;
+const Wunderbaum = mar10.Wunderbaum;
+const util = Wunderbaum.util;
+const FIXTURE_1 = [
+ {
+ title: "Node 1",
+ expanded: true,
+ children: [{ title: "Node 1.1" }, { title: "Node 1.2" }],
+ },
+ { title: "Node 2", lazy: true },
+];
+
+/* Setup */
+QUnit.testStart(function () {
+ window.sessionStorage.clear();
+ window.localStorage.clear();
+});
+
+/* Tear Down */
+QUnit.testDone(function () {});
+
+QUnit.module("Utility tests", (hooks) => {
+ test("Static utility functions", (assert) => {
+ assert.expect(2);
+
+ assert.equal(util.type([]), "array", "type([])");
+ assert.equal(util.type({}), "object", "type({})");
+ });
+});
+
+QUnit.module("Static tests", (hooks) => {
+ test("Access static properties", (assert) => {
+ assert.expect(4);
+
+ assert.true(Wunderbaum.version != null, "Statics defined");
+
+ assert.throws(
+ function () {
+ const _dummy = Wunderbaum();
+ },
+ /TypeError/,
+ "Fail if 'new' keyword is missing"
+ );
+ assert.throws(
+ function () {
+ const _dummy = new Wunderbaum();
+ },
+ /Error: Invalid 'element' option: null/,
+ "Fail if option is missing"
+ );
+ assert.throws(
+ function () {
+ const _dummy = new Wunderbaum({});
+ },
+ /Error: Invalid 'element' option: null/,
+ "Fail if 'element' option is missing"
+ );
+ });
+});
+
+QUnit.module("Instance tests", (hooks) => {
+ let tree = null;
+
+ hooks.beforeEach(() => {});
+ hooks.afterEach(() => {
+ tree.destroy();
+ tree = null;
+ });
+
+ test("Initial event sequence (fetch)", (assert) => {
+ assert.expect(5);
+ assert.timeout(1000); // Timeout after 1 second
+ const done = assert.async();
+
+ tree = new Wunderbaum({
+ element: "#tree",
+ source: "ajax-simple.json",
+ // source: FIXTURE_1,
+ receive: (e) => {
+ assert.step("receive");
+ assert.equal(
+ e.response[0].title,
+ "Node 1",
+ "receive(e) passes e.response"
+ );
+ },
+ load: (e) => {
+ assert.step("load");
+ },
+ // render: (e) => {
+ // assert.step("render");
+ // },
+ init: (e) => {
+ assert.step("init");
+ assert.verifySteps(["receive", "load", "init"], "Event sequence");
+ done();
+ },
+ });
+ });
+
+ test("Lazy load (fetch)", (assert) => {
+ assert.expect(8);
+ assert.timeout(1000); // Timeout after 1 second
+ const done = assert.async();
+ let initComplete = false;
+
+ tree = new Wunderbaum({
+ element: "#tree",
+ source: "ajax-simple.json",
+ lazyLoad: (e) => {
+ if (initComplete) {
+ assert.step("lazyLoad");
+ assert.equal(
+ e.node.title,
+ "Node 2",
+ "lazyLoad(e) passes parent node"
+ );
+ return { url: "ajax-simple-sub.json" };
+ }
+ },
+ receive: (e) => {
+ if (initComplete) {
+ assert.step("receive");
+ assert.equal(
+ e.response[0].title,
+ "SubNode 1",
+ "receive(e) passes e.response"
+ );
+ }
+ },
+ load: (e) => {
+ if (initComplete) {
+ assert.step("load");
+ assert.verifySteps(
+ ["init", "lazyLoad", "receive", "load"],
+ "Event sequence"
+ );
+ done();
+ }
+ },
+ // render: (e) => {
+ // assert.step("render");
+ // },
+ init: (e) => {
+ initComplete = true;
+ assert.step("init");
+ const lazyNode = tree.findFirst("Node 2");
+ assert.equal(lazyNode.title, "Node 2", "Find node by name");
+
+ // We need the markup, to issue a click event
+ // tree.updateViewport(true);
+ // assert.true(lazyNode.isRendered(), "Node is rendered");
+ // lazyNode.colspan.click();
+ lazyNode.setExpanded();
+ },
+ });
+ });
+
+ test("applyCommand", (assert) => {
+ assert.expect(2);
+ assert.timeout(1000); // Timeout after 1 second
+ const done = assert.async();
+
+ tree = new Wunderbaum({
+ element: "#tree",
+ source: FIXTURE_1,
+ init: (e) => {
+ const node1 = tree.findFirst("Node 1");
+ const node2 = tree.findFirst("Node 2");
+ assert.equal(node1.getPrevSibling(), null);
+
+ node1.applyCommand("moveDown");
+ assert.equal(node1.getPrevSibling(), node2);
+ // Avoid errors reported by ResizeObserver
+ done();
+ },
+ });
+ });
+ test("clones", (assert) => {
+ assert.expect(11);
+ assert.timeout(1000); // Timeout after 1 second
+ const done = assert.async();
+
+ tree = new Wunderbaum({
+ element: "#tree",
+ source: [
+ { title: "Node 1", key: "1", refKey: "n1" },
+ { title: "Node 2", key: "2", refKey: "nX" },
+ { title: "Node 3", key: "3", refKey: "nX" },
+ ],
+ init: (e) => {
+ const n1 = tree.findKey("1");
+ const n2 = tree.findKey("2");
+ const n3 = tree.findKey("3");
+
+ // console.warn(`tree.findByRefKey('nX'): >${tree.findByRefKey("nX")}<`);
+
+ assert.deepEqual(tree.findByRefKey("x"), []);
+ assert.deepEqual(tree.findByRefKey("n1"), [n1]);
+ assert.equal(tree.findByRefKey("nX").length, 2);
+
+ assert.false(n1.isClone());
+ assert.true(n2.isClone());
+ assert.true(n3.isClone());
+
+ assert.deepEqual(n1.getCloneList(), []);
+ assert.deepEqual(n1.getCloneList(true), [n1]);
+ assert.equal(n2.getCloneList().length, 1);
+ assert.equal(n2.getCloneList(false).length, 1);
+ assert.equal(n2.getCloneList(true).length, 2);
+
+ done();
+ },
+ });
+ });
+});
diff --git a/docs/unittest/test-dev.html b/docs/unittest/test-dev.html
new file mode 100644
index 0000000..179f4ef
--- /dev/null
+++ b/docs/unittest/test-dev.html
@@ -0,0 +1,28 @@
+
+
+
+
+ Test Suite (DEV) | Wunderbaum
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/unittest/test-dist.html b/docs/unittest/test-dist.html
new file mode 100644
index 0000000..943173e
--- /dev/null
+++ b/docs/unittest/test-dist.html
@@ -0,0 +1,23 @@
+
+
+
+
+ Test Suite (DIST) | Wunderbaum
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/unittest/ajax-simple-sub.json b/unittest/ajax-simple-sub.json
new file mode 100644
index 0000000..1cef475
--- /dev/null
+++ b/unittest/ajax-simple-sub.json
@@ -0,0 +1 @@
+[{ "title": "SubNode 1" }, { "title": "SubNode 2" }]
diff --git a/unittest/ajax-simple.json b/unittest/ajax-simple.json
new file mode 100644
index 0000000..76f8934
--- /dev/null
+++ b/unittest/ajax-simple.json
@@ -0,0 +1,8 @@
+[
+ {
+ "title": "Node 1",
+ "expanded": true,
+ "children": [{ "title": "Node 1.1" }, { "title": "Node 1.2" }]
+ },
+ { "title": "Node 2", "lazy": true }
+]
diff --git a/unittest/test-core-fixture1.js b/unittest/test-core-fixture1.js
new file mode 100644
index 0000000..1c14dba
--- /dev/null
+++ b/unittest/test-core-fixture1.js
@@ -0,0 +1,37 @@
+/*!
+ * Wunderbaum - Unit Test
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
+ */
+
+const { test } = QUnit;
+const Wunderbaum = mar10.Wunderbaum;
+
+const FIXTURE_1 = [
+ {
+ title: "Node 1",
+ expanded: true,
+ children: [{ title: "Node 1.1" }, { title: "Node 1.2" }],
+ },
+ { title: "Node 2", lazy: true },
+];
+
+QUnit.module("Fixture-1 tests", (hooks) => {
+ let tree;
+
+ hooks.beforeEach(() => {
+ tree = new Wunderbaum({
+ element: "#tree",
+ source: FIXTURE_1,
+ });
+ });
+ hooks.afterEach(() => {
+ tree.destroy();
+ tree = null;
+ });
+
+ test("fixture ok", (assert) => {
+ assert.expect(1);
+ assert.equal(tree.count(), 4);
+ });
+});
diff --git a/unittest/test-core.js b/unittest/test-core.js
new file mode 100644
index 0000000..cf8b6fe
--- /dev/null
+++ b/unittest/test-core.js
@@ -0,0 +1,225 @@
+/*!
+ * Wunderbaum - Unit Test
+ * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
+ * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
+ */
+/* global mar10, QUnit */
+/* eslint-env browser */
+/* eslint-disable no-console */
+
+const { test } = QUnit;
+const Wunderbaum = mar10.Wunderbaum;
+const util = Wunderbaum.util;
+const FIXTURE_1 = [
+ {
+ title: "Node 1",
+ expanded: true,
+ children: [{ title: "Node 1.1" }, { title: "Node 1.2" }],
+ },
+ { title: "Node 2", lazy: true },
+];
+
+/* Setup */
+QUnit.testStart(function () {
+ window.sessionStorage.clear();
+ window.localStorage.clear();
+});
+
+/* Tear Down */
+QUnit.testDone(function () {});
+
+QUnit.module("Utility tests", (hooks) => {
+ test("Static utility functions", (assert) => {
+ assert.expect(2);
+
+ assert.equal(util.type([]), "array", "type([])");
+ assert.equal(util.type({}), "object", "type({})");
+ });
+});
+
+QUnit.module("Static tests", (hooks) => {
+ test("Access static properties", (assert) => {
+ assert.expect(4);
+
+ assert.true(Wunderbaum.version != null, "Statics defined");
+
+ assert.throws(
+ function () {
+ const _dummy = Wunderbaum();
+ },
+ /TypeError/,
+ "Fail if 'new' keyword is missing"
+ );
+ assert.throws(
+ function () {
+ const _dummy = new Wunderbaum();
+ },
+ /Error: Invalid 'element' option: null/,
+ "Fail if option is missing"
+ );
+ assert.throws(
+ function () {
+ const _dummy = new Wunderbaum({});
+ },
+ /Error: Invalid 'element' option: null/,
+ "Fail if 'element' option is missing"
+ );
+ });
+});
+
+QUnit.module("Instance tests", (hooks) => {
+ let tree = null;
+
+ hooks.beforeEach(() => {});
+ hooks.afterEach(() => {
+ tree.destroy();
+ tree = null;
+ });
+
+ test("Initial event sequence (fetch)", (assert) => {
+ assert.expect(5);
+ assert.timeout(1000); // Timeout after 1 second
+ const done = assert.async();
+
+ tree = new Wunderbaum({
+ element: "#tree",
+ source: "ajax-simple.json",
+ // source: FIXTURE_1,
+ receive: (e) => {
+ assert.step("receive");
+ assert.equal(
+ e.response[0].title,
+ "Node 1",
+ "receive(e) passes e.response"
+ );
+ },
+ load: (e) => {
+ assert.step("load");
+ },
+ // render: (e) => {
+ // assert.step("render");
+ // },
+ init: (e) => {
+ assert.step("init");
+ assert.verifySteps(["receive", "load", "init"], "Event sequence");
+ done();
+ },
+ });
+ });
+
+ test("Lazy load (fetch)", (assert) => {
+ assert.expect(8);
+ assert.timeout(1000); // Timeout after 1 second
+ const done = assert.async();
+ let initComplete = false;
+
+ tree = new Wunderbaum({
+ element: "#tree",
+ source: "ajax-simple.json",
+ lazyLoad: (e) => {
+ if (initComplete) {
+ assert.step("lazyLoad");
+ assert.equal(
+ e.node.title,
+ "Node 2",
+ "lazyLoad(e) passes parent node"
+ );
+ return { url: "ajax-simple-sub.json" };
+ }
+ },
+ receive: (e) => {
+ if (initComplete) {
+ assert.step("receive");
+ assert.equal(
+ e.response[0].title,
+ "SubNode 1",
+ "receive(e) passes e.response"
+ );
+ }
+ },
+ load: (e) => {
+ if (initComplete) {
+ assert.step("load");
+ assert.verifySteps(
+ ["init", "lazyLoad", "receive", "load"],
+ "Event sequence"
+ );
+ done();
+ }
+ },
+ // render: (e) => {
+ // assert.step("render");
+ // },
+ init: (e) => {
+ initComplete = true;
+ assert.step("init");
+ const lazyNode = tree.findFirst("Node 2");
+ assert.equal(lazyNode.title, "Node 2", "Find node by name");
+
+ // We need the markup, to issue a click event
+ // tree.updateViewport(true);
+ // assert.true(lazyNode.isRendered(), "Node is rendered");
+ // lazyNode.colspan.click();
+ lazyNode.setExpanded();
+ },
+ });
+ });
+
+ test("applyCommand", (assert) => {
+ assert.expect(2);
+ assert.timeout(1000); // Timeout after 1 second
+ const done = assert.async();
+
+ tree = new Wunderbaum({
+ element: "#tree",
+ source: FIXTURE_1,
+ init: (e) => {
+ const node1 = tree.findFirst("Node 1");
+ const node2 = tree.findFirst("Node 2");
+ assert.equal(node1.getPrevSibling(), null);
+
+ node1.applyCommand("moveDown");
+ assert.equal(node1.getPrevSibling(), node2);
+ // Avoid errors reported by ResizeObserver
+ done();
+ },
+ });
+ });
+ test("clones", (assert) => {
+ assert.expect(11);
+ assert.timeout(1000); // Timeout after 1 second
+ const done = assert.async();
+
+ tree = new Wunderbaum({
+ element: "#tree",
+ source: [
+ { title: "Node 1", key: "1", refKey: "n1" },
+ { title: "Node 2", key: "2", refKey: "nX" },
+ { title: "Node 3", key: "3", refKey: "nX" },
+ ],
+ init: (e) => {
+ const n1 = tree.findKey("1");
+ const n2 = tree.findKey("2");
+ const n3 = tree.findKey("3");
+
+ // console.warn(`tree.findByRefKey('nX'): >${tree.findByRefKey("nX")}<`);
+
+ assert.deepEqual(tree.findByRefKey("x"), []);
+ assert.deepEqual(tree.findByRefKey("n1"), [n1]);
+ assert.equal(tree.findByRefKey("nX").length, 2);
+
+ assert.false(n1.isClone());
+ assert.true(n2.isClone());
+ assert.true(n3.isClone());
+
+ assert.deepEqual(n1.getCloneList(), []);
+ assert.deepEqual(n1.getCloneList(true), [n1]);
+ assert.equal(n2.getCloneList().length, 1);
+ assert.equal(n2.getCloneList(false).length, 1);
+ assert.equal(n2.getCloneList(true).length, 2);
+
+ done();
+ },
+ });
+ });
+});
diff --git a/unittest/test-dev.html b/unittest/test-dev.html
new file mode 100644
index 0000000..179f4ef
--- /dev/null
+++ b/unittest/test-dev.html
@@ -0,0 +1,28 @@
+
+
+
+
+ Test Suite (DEV) | Wunderbaum
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/unittest/test-dist.html b/unittest/test-dist.html
new file mode 100644
index 0000000..943173e
--- /dev/null
+++ b/unittest/test-dist.html
@@ -0,0 +1,23 @@
+
+
+
+
+ Test Suite (DIST) | Wunderbaum
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+