Implemented the Organization Chart feature to create customer organizational

This commit is contained in:
2026-10-26 18:15:32 -03:00
parent bf48ed1dcb
commit 3229ce9c41
41 changed files with 4099 additions and 63 deletions

81
tests/orgChart.test.cjs Normal file
View File

@@ -0,0 +1,81 @@
const { test } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const vm = require("node:vm");
const { loadBackground } = require("./support/characterization.cjs");
const factory = (file, name) => new vm.Script(fs.readFileSync(file, "utf8") + "\n" + name).runInNewContext();
const model = factory("src/features/org-chart/model.js", "createOrgChartModel")();
const person = (id, parentId = null) => ({ id, parentId, name: id, role: "Director", notes: "", collapsed: false });
const plain = value => JSON.parse(JSON.stringify(value));
test("contact PartyNumber survives normalization and edits while legacy nodes remain supported", () => {
const nodes = model.normalize([{...person("a"), contactPartyId:"123", PartyNumber:18725565}]);
assert.equal(nodes[0].PartyNumber,"18725565");
assert.equal(model.normalize([{...person("alpha"), PartyNumber:"A4KCS2P"}])[0].PartyNumber,"A4KCS2P");
assert.equal(model.upsert(nodes,{...nodes[0],name:"Updated"})[0].PartyNumber,"18725565");
assert.equal(model.normalize([person("legacy")])[0].PartyNumber,undefined);
assert.throws(()=>model.normalize([{...person("a"),PartyNumber:"bad'query"}]),/PartyNumber/);
});
test("organization charts validate IDs, parents, cycles, required fields and limits", () => {
const nodes = [person("a"), person("b", "a"), person("c", "b")];
assert.equal(model.normalize(nodes).length, 3);
for (const invalid of [
[person("a"), person("a")], [person("a", "missing")], [person("a", "a")],
[person("a", "b"), person("b", "a")], [{ ...person("a"), name: " " }],
[{ ...person("a"), notes: "x".repeat(4001) }],
Array.from({ length: 501 }, (_, i) => person("p" + i))
]) assert.throws(() => model.normalize(invalid));
assert.throws(() => model.upsert(nodes, { ...nodes[0], parentId: "c" }), /cycle/);
assert.deepEqual([...model.descendants(nodes, "b")], ["b", "c"]);
});
test("editing and deleting preserve other people and promote direct reports without losing notes", () => {
const nodes = [person("a"), { ...person("b", "a"), notes: "<script>literal text</script>" }, person("c", "b")];
const before = JSON.stringify(nodes);
const updated = model.upsert(nodes, { ...nodes[1], name: "Changed" });
assert.equal(updated[1].name, "Changed");
const removed = model.remove(updated, "a");
assert.equal(removed[0].parentId, null);
assert.equal(removed[1].parentId, "b");
assert.equal(removed[0].notes, nodes[1].notes);
assert.equal(JSON.stringify(nodes), before);
});
test("collapsed branches omit descendants from layout, not persisted data", () => {
const nodes = [person("a"), { ...person("b", "a"), collapsed: true }, person("c", "b"), person("d")];
const root = model.hierarchy(nodes);
assert.deepEqual(plain(root.children.map(node => node.id)), ["a", "d"]);
assert.equal(root.children[0].children[0].children.length, 0);
assert.equal(nodes.length, 4);
});
test("contact identity survives persistence while legacy people remain valid", () => {
const nodes = model.normalize([person("legacy"), { ...person("contact"), contactPartyId: "300008443263169" }]);
assert.equal(nodes[0].contactPartyId, undefined);
assert.equal(nodes[1].contactPartyId, "300008443263169");
assert.throws(() => model.normalize([{ ...person("a"), contactPartyId: "" }]));
});
test("normalization persists Job Title but never email address", () => {
const [node] = model.normalize([{ ...person("a"), emailAddress: "test@example.invalid", jobTitle: "Director" }]);
assert.equal(Object.hasOwn(node, "role"), false);
assert.equal(Object.hasOwn(node, "emailAddress"), false);
assert.equal(node.jobTitle, "Director");
});
test("organization storage rejects invalid input and unavailable IndexedDB without claiming a save", async () => {
const store = factory("src/features/org-chart/storage.js", "createOrgChartStorage")({ environment: {}, model });
await assert.rejects(store.load({ principal: "", customerId: "1" }), /current user/);
await assert.rejects(store.load({ principal: "test@example.invalid", customerId: "1" }), /IndexedDB/);
await assert.rejects(store.save({ principal: "test@example.invalid", customerId: "1", revision: 0, nodes: [person("x", "missing")] }), /missing manager/);
});
for (const family of ["chrome", "firefox"]) test(family + ": organization messages return storage errors through the existing reply contract", async () => {
const app = loadBackground({ family });
for (const operation of ["loadOrganizationChart", "saveOrganizationChart"]) {
const result = await app.dispatch({ type: "opportunitiesExtension." + operation, principal: "test@example.invalid", customerId: "1", revision: 0, nodes: [] });
assert.equal(result.ok, false);
assert.match(result.error, /IndexedDB/);
}
});