Implemented the functionality to add a WL Action

This commit is contained in:
2026-09-28 17:32:32 -03:00
parent 1a3decbeb6
commit a182609415
23 changed files with 1981 additions and 79 deletions

View File

@@ -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);
});