const assert = require("node:assert/strict"); const { test } = require("node:test"); const { loadApplication } = require("./support/application.cjs"); const flush = () => new Promise(resolve => setImmediate(resolve)); const lookupData = () => Object.fromEntries(["ROLE", "NAME", "TEAM", "STATUS", "COMPLEXITY"].map(key => [ `WWB_ACTION_${key}`, [{ code: `${key}_CODE`, name: `${key} ` }] ])); function setup() { let document; class Element { constructor(tag) { this.tag = tag; this.children = []; this.attributes = {}; this.listeners = {}; this.isConnected = true; this.text = ""; } set textContent(value) { this.text = value; this.children = []; } get textContent() { return this.text + this.children.map(child => child.textContent).join(""); } append(...nodes) { for (const node of nodes) { node.parent = this; this.children.push(node); } } setAttribute(key, value) { this.attributes[key] = value; } addEventListener(key, fn) { this.listeners[key] = fn; } focus() { document.activeElement = this; } remove() { this.parent.children = this.parent.children.filter(child => child !== this); this.isConnected = false; } click() { this.listeners.click?.({ target: this, preventDefault() {}, stopPropagation() {} }); } } const host = new Element("host"); document = { createElement: tag => new Element(tag), createElementNS: (_, tag) => new Element(tag), getElementById: id => id === "opportunities-extension-modal" ? host : null, listeners: {}, addEventListener(key, fn) { this.listeners[key] = fn; } }; const app = loadApplication("content", { document, window: {} }); app.currentUser.allowsCrmActivities = () => true; app.authSessionState.principal = "synthetic@example.invalid"; app.productsContentState.opptyProductsByNumber.set("A", { items: [] }); app.productsContentState.opptyProductsByNumber.set("B", { items: [] }); app.platformContentMessages.sendRuntimeMessage = () => assert.fail("Form must not send requests"); const walk = node => [node, ...node.children.flatMap(walk)]; const nodes = () => walk(host); const form = () => nodes().find(node => node.tag === "form"); return { app, host, document, nodes, form }; } test("only plus opens the nine-field form; a missing workload stays disabled without a save operation", () => { const h = setup(); const badge = h.app.opportunityWorkloadActions.createBadge("A"); assert.equal(badge.textContent, "0+"); badge.click(); assert.equal(h.host.children.length, 0); const plus = badge.children.find(node => node.tag === "button"); assert.equal(plus.textContent, "+"); assert.equal(plus.attributes["aria-haspopup"], "dialog"); plus.click(); assert.equal(h.host.children.length, 1); assert.equal(h.host.children[0].attributes.role, "dialog"); assert.deepEqual(h.nodes().filter(node => node.name).map(node => node.name), ["owner", "role", "action", "team", "startDate", "endDate", "status", "complexity", "notes"]); assert.equal(h.nodes().find(node => node.name === "notes").tag, "textarea"); assert.equal(h.nodes().find(node => node.name === "startDate").type, "date"); assert.equal(h.document.activeElement.title, "Close"); assert.ok(h.nodes().filter(node => node.name).every(node => node.disabled)); assert.equal(h.form().attributes["data-state"], "error"); assert.equal(h.nodes().find(node => node.textContent === "Save").disabled, true); let prevented = false; h.form().listeners.submit({ preventDefault() { prevented = true; } }); assert.equal(prevented, true); h.nodes().find(node => node.textContent === "Cancel").click(); assert.equal(h.host.children.length, 0); assert.equal(h.document.activeElement, plus); }); test("form traps focus, handles Escape, replaces rather than duplicates, and resets its fields", () => { const h = setup(); h.app.contentModal.ensureEscapeKeyHandler(); const first = h.app.opportunityWorkloadActions.createBadge("A").children.find(node => node.tag === "button"); first.click(); h.nodes().find(node => node.name === "notes").value = "Draft"; const second = h.app.opportunityWorkloadActions.createBadge("B").children.find(node => node.tag === "button"); second.click(); assert.equal(h.host.children.length, 1); assert.equal(h.nodes().find(node => node.name === "notes").value, undefined); const cancel = h.nodes().find(node => node.textContent === "Cancel"); cancel.focus(); let prevented = false; h.host.children[0].listeners.keydown({ key: "Tab", preventDefault() { prevented = true; } }); assert.equal(prevented, true); assert.equal(h.document.activeElement.title, "Close"); h.document.listeners.keydown({ key: "Escape", preventDefault() {} }); assert.equal(h.host.children.length, 0); assert.equal(h.document.activeElement, second); }); test("SALES_REP, loading/unknown Actions and stale triggers cannot open the modal", () => { const h = setup(); const plus = h.app.opportunityWorkloadActions.createBadge("A").children.find(node => node.tag === "button"); h.app.currentUser.allowsCrmActivities = () => false; plus.click(); assert.equal(h.host.children.length, 0); assert.equal(h.app.opportunityWorkloadActions.createBadge("A"), null); h.app.currentUser.allowsCrmActivities = () => true; h.app.productsContentState.opptyProductsByNumber.set("A", { items: [{ RevnId: "pending" }] }); plus.click(); assert.equal(h.host.children.length, 0); assert.equal(h.app.opportunityWorkloadActions.createBadge("A").children.some(node => node.tag === "button"), false); h.app.authSessionState.principal = ""; assert.equal(h.app.opportunityWorkloadActions.createBadge("B").children.some(node => node.tag === "button"), false); }); test("resetting Actions closes an open form without leaving an old opportunity context", () => { const h = setup(); h.app.opportunityWorkloadActions.createBadge("A").children.find(node => node.tag === "button").click(); h.app.opportunityWorkloadActions.reset(); assert.equal(h.host.children.length, 0); }); test("Action header shows the selected opportunity customer, name and formatted close date safely", () => { const h = setup(); h.app.opportunitiesTableState.opportunitiesTableState.items = [ { OptyNumber: "A", Name: "", CustomerAccount: { PartyUniqueName: "Customer A" }, EffectiveDate: "2026-11-30" }, { OptyNumber: "B", Name: "Opportunity B", CustomerAccount: { PartyUniqueName: "Customer B" }, EffectiveDate: null } ]; h.app.opportunityWorkloadActions.createBadge("A").children.find(node => node.tag === "button").click(); assert.deepEqual(h.nodes().filter(node => node.tag === "dt").map(node => node.textContent), ["Customer", "Name", "Close Date"]); assert.deepEqual(h.nodes().filter(node => node.tag === "dd").map(node => node.textContent), ["Customer A", "", "30/11/2026"]); assert.equal(h.nodes().some(node => node.tag === "script"), false); h.app.opportunityWorkloadActions.createBadge("B").children.find(node => node.tag === "button").click(); assert.deepEqual(h.nodes().filter(node => node.tag === "dd").map(node => node.textContent), ["Customer B", "Opportunity B", "-"]); }); function optionsSetup(products = [{ RevnId: "123", WorkloadName_c: "Synthetic workload" }]) { const h = setup(); h.calls = []; h.app.productsContentState.opptyProductsByNumber.set("A", { items: products }); h.app.opportunityWorkloadActions.getSummary = () => ({ state: "missing" }); h.app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic-wwb" } }); h.app.debugContentView.renderDebugPanel = () => {}; h.app.platformContentMessages.sendRuntimeMessage = message => new Promise((resolve, reject) => h.calls.push({ message, resolve, reject })); h.field = name => h.nodes().find(node => node.name === name); h.open = () => h.app.workloadActionForm.open("A", null); h.workload = () => h.nodes().find(node => node.id === "opportunities-extension-action-form-workload"); h.completeLookup = () => h.calls.find(call => call.message.type.endsWith("requestActionFindLookup")).resolve({ ok: true, lookups: lookupData(), debugRequests: [] }); return h; } test("both options requests must succeed before fields enable; email and lookup codes stay distinct from labels", async () => { const h = optionsSetup(); h.open(); assert.equal(h.form().attributes["aria-busy"], "true"); assert.ok(h.nodes().filter(node => node.name).every(node => node.disabled)); await flush(); assert.equal(h.calls.length, 2); assert.deepEqual(h.calls.map(call => call.message.type), ["opportunitiesExtension.requestActionFindLookup", "opportunitiesExtension.requestActionTeamMembers"]); const team = h.calls[1]; assert.equal(team.message.workloadId, "123"); for (const call of h.calls) assert.equal(call.message.authorization.value, "synthetic-wwb"); team.resolve({ ok: true, members: [{ email: "owner@example.invalid" }], debugRequests: [] }); await flush(); assert.equal(h.field("owner").disabled, true, "a partial result cannot enable the form"); h.completeLookup(); await flush(); assert.equal(h.form().attributes["data-state"], "ready"); assert.equal(h.form().attributes["aria-busy"], "false"); assert.equal(h.field("owner").tag, "select"); assert.equal(h.field("owner").children[1].textContent, "owner@example.invalid"); assert.equal(h.field("owner").children[1].value, "owner@example.invalid"); assert.equal(h.field("owner").value, "", "unmatched user is not replaced by the first owner"); for (const [name, key] of [["role", "ROLE"], ["action", "NAME"], ["team", "TEAM"], ["status", "STATUS"], ["complexity", "COMPLEXITY"]]) { const field = h.field(name); assert.equal(field.tag, "select"); assert.equal(field.disabled, false); assert.equal(field.children[1].value, `${key}_CODE`); assert.equal(field.children[1].textContent, `${key} `); assert.equal(field.children[1].children.length, 0); } assert.equal(h.document.activeElement.id, `${h.field("owner").id}-search`); assert.equal(h.nodes().find(node => node.textContent === "Save").disabled, false); let prevented = false; h.form().listeners.submit({ preventDefault() { prevented = true; } }); assert.equal(prevented, true); assert.equal(h.calls.length, 2, "no save request"); }); test("Action defaults use current user and lookup labels; only Notes is optional", async () => { const h = optionsSetup(); const identity = h.app.currentUser.ensure(); h.calls[0].resolve({ ok: true, user: { id: "OWNER@example.invalid", roles: ["SIMPLE_USER"] } }); await identity; assert.equal(h.app.currentUser.getUserId(), "OWNER@example.invalid"); h.calls.length = 0; h.open(); await flush(); const data = lookupData(); data.WWB_ACTION_ROLE = [{ code: "CLOUD_ARCHITECT", name: "Cloud Architect" }]; data.WWB_ACTION_NAME = [{ code: "SUCCESS_PLAN_CONSUM_RVW", name: "Success Plan & Consumption review" }]; data.WWB_ACTION_STATUS = [{ code: "COMPLETE", name: "Complete" }]; h.calls[0].resolve({ ok: true, lookups: data }); h.calls[1].resolve({ ok: true, members: [{ email: "other@example.invalid" }, { email: "owner@example.invalid" }] }); await flush(); for (const [name, value, label] of [ ["owner", "owner@example.invalid", "owner@example.invalid"], ["role", "CLOUD_ARCHITECT", "Cloud Architect"], ["action", "SUCCESS_PLAN_CONSUM_RVW", "Success Plan & Consumption review"], ["status", "COMPLETE", "Complete"] ]) { assert.equal(h.field(name).value, value); assert.equal(h.nodes().find(node => node.id === `${h.field(name).id}-search`).value, label); } for (const name of ["owner", "role", "action", "team", "startDate", "endDate", "status", "complexity"]) { const field = h.field(name); assert.equal(field.required, true); assert.equal(field.attributes["aria-required"], "true"); assert.ok(field.parent.children[0].textContent.endsWith("(required)")); if (field.tag === "select") { assert.equal(h.nodes().find(node => node.id === `${field.id}-search`).required, true); } } assert.equal(h.field("team").value, ""); assert.equal(h.field("complexity").value, ""); assert.equal(h.field("notes").required, false); assert.equal(h.field("notes").parent.children[0].textContent, "Notes (optional)"); assert.equal(h.calls.length, 2); }); test("Save validates, prevents duplicate POSTs, preserves errors and reports success", async () => { const h = optionsSetup(); h.app.productsContentJobs.queueProductsTableRender = () => {}; const notifications = []; h.app.opportunitiesLinks.showCopyConfirmation = (...args) => notifications.push(args); h.app.currentUser.getUserId = () => "creator@example.invalid"; h.open(); await flush(); h.completeLookup(); h.calls[1].resolve({ ok: true, members: [{ email: "owner@example.invalid" }] }); await flush(); const submit = () => h.form().listeners.submit({ preventDefault() {} }); await submit(); assert.equal(h.calls.length, 2); assert.equal(h.nodes().find(node => node.id === `${h.field("owner").id}-search`).attributes["aria-invalid"], "true"); for (const name of ["owner", "role", "action", "team", "status", "complexity"]) h.field(name).value = h.field(name).children[1].value; h.field("startDate").value = "2026-09-14"; h.field("endDate").value = "2026-09-13"; await submit(); assert.equal(h.calls.length, 2); assert.equal(h.field("endDate").attributes["aria-invalid"], "true"); h.field("endDate").value = "2026-09-15"; h.field("notes").value = "My note"; const first = submit(); await flush(); await submit(); assert.equal(h.calls.length, 3); const call = h.calls[2]; assert.equal(call.message.type, "opportunitiesExtension.requestMgmtAction"); assert.equal(call.message.authorization.value, "synthetic-wwb"); assert.deepEqual(JSON.parse(JSON.stringify(call.message.payload)), { parentType: "WORKLOAD", parentId: "123", action: { ownerEmail: "owner@example.invalid", role: "ROLE_CODE", name: "NAME_CODE", team: "TEAM_CODE", tags: "", startDate: "2026-09-14T00:00:00.000Z", endDate: "2026-09-15T00:00:00.000Z", status: "STATUS_CODE", complexity: "COMPLEXITY_CODE", notes: ["My note"], objective: "", createdBy: "creator@example.invalid", updatedBy: "creator@example.invalid" } }); call.resolve({ ok: false, error: "HTTP 403" }); await first; assert.equal(h.field("notes").value, "My note"); assert.deepEqual(notifications[0], ["HTTP 403", true]); h.field("notes").value = ""; const second = submit(); await flush(); assert.equal(h.calls[3].message.payload.action.notes.length, 0); h.calls[3].resolve({ ok: true, result: { id: 77, ownerEmail: "synthetic@example.invalid", name: "REVIEW" } }); await second; assert.equal(h.host.children.length, 0); assert.deepEqual(notifications[1], ["Action saved successfully."]); assert.equal(h.app.opportunityWorkloadActions.createBadge("A").attributes["data-state"], "matched"); }); test("autocomplete searches labels and codes, selects by keyboard and restores committed values", async () => { const h = optionsSetup(); h.open(); await flush(); const data = lookupData(); data.WWB_ACTION_ROLE = [{ code: "CA", name: "Cloud Architect" }, { code: "SE", name: "Solution Engineer" }]; h.calls[0].resolve({ ok: true, lookups: data }); h.calls[1].resolve({ ok: true, members: [{ email: "owner@example.invalid" }] }); await flush(); const input = h.nodes().find(node => node.id === `${h.field("role").id}-search`); const list = h.nodes().find(node => node.id === input.attributes["aria-controls"]); list.style = {}; input.closest = () => ({ getBoundingClientRect: () => ({ top: 20, bottom: 600 }) }); input.getBoundingClientRect = () => ({ top: 100, bottom: 138 }); const key = key => input.listeners.keydown({ key, preventDefault() {}, stopPropagation() {} }); input.value = "architect"; input.listeners.input(); assert.equal(list.attributes["data-placement"], "below"); assert.equal(list.style.maxHeight, "180px"); assert.equal(list.children.length, 1); assert.equal(list.children[0].textContent, "Cloud Architect"); key("ArrowDown"); assert.equal(list.children[0].attributes["aria-selected"], "true"); key("Enter"); assert.equal(h.field("role").value, "CA"); assert.equal(input.value, "Cloud Architect"); assert.equal(list.hidden, true); input.value = "not present"; input.getBoundingClientRect = () => ({ top: 500, bottom: 538 }); input.listeners.input(); assert.equal(list.attributes["data-placement"], "above"); assert.equal(list.style.maxHeight, "180px"); assert.equal(list.textContent, "No results"); key("Escape"); assert.equal(input.value, "Cloud Architect"); input.value = "SE"; input.listeners.input(); list.children[0].click(); assert.equal(h.field("role").value, "SE"); assert.equal(input.value, "Solution Engineer"); input.value = "free text"; input.listeners.blur(); assert.equal(input.value, "Solution Engineer"); assert.equal(h.calls.length, 2, "searching never issues requests"); }); test("workload autocomplete uses the selected RevnId and disables metadata search during loading", async () => { const h = optionsSetup([{ RevnId: "123", WorkloadName_c: "First" }, { RevnId: "456", WorkloadName_c: "Second" }]); h.open(); await flush(); const input = h.nodes().find(node => node.id === `${h.workload().id}-search`); input.value = "second"; input.listeners.input(); input.listeners.keydown({ key: "Enter", preventDefault() {} }); await flush(); assert.equal(h.workload().value, "456"); assert.equal(h.calls[1].message.workloadId, "456"); assert.ok(h.nodes().filter(node => node.attributes.role === "combobox" && node !== input).every(node => node.disabled)); }); test("multiple workloads require selection, share the lookup and ignore an older member response", async () => { const h = optionsSetup([{ RevnId: "123", WorkloadName_c: "First" }, { RevnId: "456", WorkloadName_c: "Second" }, { RevnId: "123" }]); h.open(); await flush(); assert.equal(h.calls.length, 1); assert.equal(h.form().attributes["data-state"], "waiting"); const workload = h.workload(); assert.equal(workload.children.length, 3, "duplicate revenue IDs do not duplicate workloads"); assert.equal(workload.parent.hidden, false); workload.value = "123"; workload.listeners.change(); await flush(); workload.value = "456"; workload.listeners.change(); await flush(); assert.deepEqual(h.calls.slice(1).map(call => call.message.workloadId), ["123", "456"]); h.completeLookup(); h.calls[2].resolve({ ok: true, members: [{ email: "second@example.invalid" }] }); await flush(); assert.equal(h.field("owner").children[1].value, "second@example.invalid"); h.calls[1].resolve({ ok: true, members: [{ email: "first@example.invalid" }] }); await flush(); assert.equal(h.field("owner").children[1].value, "second@example.invalid"); assert.equal(h.form().attributes["data-state"], "ready"); }); test("errors remain local, register diagnostics under Actions and can be retried without duplicate requests", async () => { const h = optionsSetup(); h.open(); await flush(); h.completeLookup(); h.calls[1].resolve({ ok: false, error: "Synthetic HTTP 403", debugRequests: [{ name: "requestActionTeamMembers", status: "failed", httpStatus: 403, startedAt: "2026-09-28T12:00:00Z", completedAt: "2026-09-28T12:00:01Z", durationMs: 1000, requestHeaders: { "Wwb-Provider-Authorization": "(present)" } }] }); await flush(); assert.equal(h.form().attributes["data-state"], "error"); assert.equal(h.field("owner").disabled, true); assert.equal(h.field("owner").children[0].textContent, "Unavailable"); const retry = h.nodes().find(node => node.textContent === "Retry"); assert.equal(retry.hidden, false); assert.equal(h.app.debugState.requestLog.length, 1); assert.equal(h.app.debugContentView.getRequestCategory(h.app.debugState.requestLog[0]), "actions"); assert.equal(h.app.debugContentView.getRequestCategory({ name: "requestActionFindLookup" }), "actions"); retry.click(); retry.click(); await flush(); assert.equal(h.calls.length, 4); h.calls[2].resolve({ ok: true, lookups: lookupData() }); h.calls[3].resolve({ ok: true, members: [] }); await flush(); assert.equal(h.form().attributes["data-state"], "ready"); assert.equal(h.field("owner").disabled, true); assert.equal(h.field("owner").children[0].textContent, "No options available"); assert.equal(h.field("role").disabled, false); }); test("closing or reopening the form discards late responses and waits for fresh options", async () => { const h = optionsSetup(); h.open(); await flush(); const previous = h.form(); h.app.workloadActionForm.close(false); h.open(); await flush(); h.completeLookup(); h.calls[1].resolve({ ok: true, members: [{ email: "old@example.invalid" }] }); await flush(); assert.notEqual(h.form(), previous); assert.equal(h.form().attributes["data-state"], "loading"); h.calls[2].resolve({ ok: true, lookups: lookupData() }); h.calls[3].resolve({ ok: true, members: [{ email: "fresh@example.invalid" }] }); await flush(); assert.equal(h.field("owner").children[1].value, "fresh@example.invalid"); }); test("closing before authentication finishes prevents both options requests", async () => { const h = optionsSetup(); let finish; const auth = new Promise(resolve => { finish = resolve; }); h.app.authContentSession.requestConsumerTokenServiceOnce = () => auth; h.open(); h.app.workloadActionForm.close(false); finish({ workloadActions: { value: "synthetic" } }); await flush(); assert.equal(h.calls.length, 0); });