Implemented the functionality to add a WL Action
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
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} <synthetic>` }]
|
||||
]));
|
||||
|
||||
function setup() {
|
||||
let document;
|
||||
@@ -31,7 +35,7 @@ function setup() {
|
||||
return { app, host, document, nodes, form };
|
||||
}
|
||||
|
||||
test("only plus on the missing Action badge opens a form with six fields and no save operation", () => {
|
||||
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+");
|
||||
@@ -43,10 +47,12 @@ test("only plus on the missing Action badge opens a form with six fields and no
|
||||
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), ["team", "startDate", "endDate", "status", "complexity", "notes"]);
|
||||
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.name, "team");
|
||||
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; } });
|
||||
@@ -113,3 +119,297 @@ test("Action header shows the selected opportunity customer, name and formatted
|
||||
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} <synthetic>`);
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user