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: "" }, 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/); } });