Implemented the functionality to add a WL Action
This commit is contained in:
110
tests/actionOptions.test.cjs
Normal file
110
tests/actionOptions.test.cjs
Normal file
@@ -0,0 +1,110 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const { loadBackground, response } = require("./support/characterization.cjs");
|
||||
const lookups = () => Object.fromEntries(["ROLE", "NAME", "TEAM", "STATUS", "COMPLEXITY"].map(key => [
|
||||
`WWB_ACTION_${key}`, [{ code: `${key}_CODE`, name: `${key} label` }]
|
||||
]));
|
||||
const teamType = "opportunitiesExtension.requestActionTeamMembers";
|
||||
const lookupType = "opportunitiesExtension.requestActionFindLookup";
|
||||
|
||||
test("Save sends the WWB POST, accepts empty success and never retries errors", async () => {
|
||||
const payload = { parentType: "WORKLOAD", parentId: "123", action: {
|
||||
ownerEmail: "owner@example.invalid", role: "CA", name: "REVIEW", team: "TEAM", tags: "",
|
||||
startDate: "2026-09-14T00:00:00.000Z", endDate: "2026-09-15T00:00:00.000Z",
|
||||
status: "COMPLETE", complexity: "LOW", notes: [], objective: "",
|
||||
createdBy: "creator@example.invalid", updatedBy: "creator@example.invalid"
|
||||
} };
|
||||
for (const status of [200, 201, 204, 403, 500]) {
|
||||
const calls = [];
|
||||
const h = loadBackground({ family: "chrome", fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
return status === 204 ? new Response(null, { status }) : response({ id: 123 }, status);
|
||||
} });
|
||||
const result = await h.dispatch({ type: "opportunitiesExtension.requestMgmtAction", payload, authorization: { value: "secret-wwb" } });
|
||||
assert.equal(result.ok, status < 300, `${status}: ${result.error}`);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].url.endsWith("/wwb/mgmt/actions"));
|
||||
assert.equal(calls[0].options.method, "POST");
|
||||
assert.equal(calls[0].options.headers["Wwb-Provider-Authorization"], "secret-wwb");
|
||||
assert.deepEqual(JSON.parse(calls[0].options.body), payload);
|
||||
assert.ok(!JSON.stringify(result).includes("secret-wwb"));
|
||||
}
|
||||
});
|
||||
|
||||
for (const family of ["chrome", "firefox"]) {
|
||||
test(`${family}: Action options use dynamic RevnId, WWB-TS and masked GET diagnostics`, async () => {
|
||||
const calls = [];
|
||||
const h = loadBackground({ family, fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
if (url.endsWith("consumerTokenService")) return response([
|
||||
{ tokenService: "ACCP-TS", uiaasHeader: { value: "summary-token" } },
|
||||
{ tokenService: "OCICONS-TS", uiaasHeader: { value: "wrong-token" } },
|
||||
{ tokenService: "WWB-TS", uiaasHeader: { value: "synthetic-wwb" } }
|
||||
]);
|
||||
return response(url.includes("teamMembers") ? [{ email: "owner@example.invalid" }] : lookups());
|
||||
} });
|
||||
const auth = await h.dispatch({ type: "opportunitiesExtension.requestConsumerTokenService" });
|
||||
const authorization = auth.authorizations.workloadActions;
|
||||
const team = await h.dispatch({ type: teamType, workloadId: "987654", authorization, url: "https://example.invalid" });
|
||||
const lookup = await h.dispatch({ type: lookupType, authorization });
|
||||
assert.equal(team.ok, true);
|
||||
assert.deepEqual(team.members, [{ email: "owner@example.invalid" }]);
|
||||
assert.deepEqual(lookup.lookups, lookups());
|
||||
const base = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/workloads";
|
||||
assert.equal(calls[1].url, `${base}/987654/teamMembers?includeLeads=true`);
|
||||
assert.equal(calls[2].url, `${base}/fndLookup`);
|
||||
for (const call of calls.slice(1)) {
|
||||
assert.equal(call.options.method, "GET");
|
||||
assert.equal(call.options.credentials, "include");
|
||||
assert.deepEqual({ ...call.options.headers }, { Accept: "application/json", "Wwb-Provider-Authorization": "synthetic-wwb" });
|
||||
}
|
||||
for (const [result, name] of [[team, "requestActionTeamMembers"], [lookup, "requestActionFindLookup"]]) {
|
||||
const log = result.debugRequests[0];
|
||||
assert.equal(log.name, name);
|
||||
assert.equal(log.status, "success");
|
||||
assert.equal(log.httpStatus, 200);
|
||||
assert.ok(log.durationMs >= 0);
|
||||
assert.equal(log.requestHeaders["Wwb-Provider-Authorization"], "(present)");
|
||||
assert.doesNotMatch(JSON.stringify(result), /synthetic-wwb|wrong-token/);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("invalid workload IDs and missing authorization never fetch or accept an arbitrary target", async () => {
|
||||
const h = loadBackground({ fetch: () => assert.fail("No network expected") });
|
||||
for (const workloadId of [undefined, "", "abc", "12/../fndLookup", "12?other=1"]) {
|
||||
assert.equal((await h.dispatch({ type: teamType, workloadId, authorization: { value: "synthetic" } })).ok, false);
|
||||
}
|
||||
for (const type of [teamType, lookupType]) {
|
||||
assert.equal((await h.dispatch({ type, workloadId: "123" })).ok, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("Action options require exactly HTTP 200 and valid collections and preserve failed debug entries", async () => {
|
||||
for (const type of [teamType, lookupType]) {
|
||||
const valid = type === teamType ? [{ email: "owner@example.invalid" }] : lookups();
|
||||
for (const fetch of [
|
||||
async () => response(valid, 201), async () => response({}, 401), async () => response({}, 403),
|
||||
async () => { throw new Error("Synthetic network failure"); },
|
||||
async () => new Response("<html>Unexpected session page</html>"),
|
||||
async () => response(type === teamType ? [{ name: "Missing email" }] : { ...lookups(), WWB_ACTION_NAME: null })
|
||||
]) {
|
||||
const h = loadBackground({ fetch });
|
||||
const result = await h.dispatch({ type, workloadId: "123", authorization: { value: "synthetic-secret" } });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.debugRequests.length, 1);
|
||||
assert.equal(result.debugRequests[0].status, "failed");
|
||||
assert.ok(result.error);
|
||||
assert.doesNotMatch(JSON.stringify(result), /synthetic-secret/);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("empty option lists are valid responses, not invented defaults", async () => {
|
||||
const empty = Object.fromEntries(Object.keys(lookups()).map(key => [key, []]));
|
||||
const h = loadBackground({ fetch: async url => response(url.includes("teamMembers") ? [] : empty) });
|
||||
const members = await h.dispatch({ type: teamType, workloadId: "123", authorization: { value: "synthetic" } });
|
||||
const lookup = await h.dispatch({ type: lookupType, authorization: { value: "synthetic" } });
|
||||
assert.deepEqual(members.members, []);
|
||||
assert.deepEqual(lookup.lookups, empty);
|
||||
});
|
||||
@@ -37,7 +37,7 @@ test("activity badge filters precede Status, are accessible and combine without
|
||||
assert.equal(h.button("sr", "matched").textContent, "");
|
||||
assert.equal(h.button("actions", "missing").textContent, "");
|
||||
assert.equal(h.button("actions", "matched").type, "button");
|
||||
assert.equal(h.controls.children[1].children[0].textContent, "Act");
|
||||
assert.equal(h.controls.children[1].children[0].textContent, "Action");
|
||||
for (const value of ["matched", "missing"]) {
|
||||
assert.equal(h.button("actions", value).textContent, "");
|
||||
assert.ok(h.button("actions", value).title.startsWith("Whether the Action is registered in Workload Workbench.\n"));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -64,6 +64,45 @@ function contentFixture(send) {
|
||||
return app;
|
||||
}
|
||||
|
||||
test("saved response updates the workload badge, tooltip and matching cache without fetching", async () => {
|
||||
const app = contentFixture(() => assert.fail("Save response must not trigger a GET"));
|
||||
const writes = [];
|
||||
app.activityCache.write = async (...args) => writes.push(args);
|
||||
const products = { items: [{ RevnId: "123" }], cachedAt: 1000 };
|
||||
app.productsContentState.opptyProductsByNumber.set("A", products);
|
||||
app.productsContentState.opptyProductsByNumber.set("B", { items: [{ RevnId: "456" }] });
|
||||
const action = { id: 77, name: "REVIEW", ownerEmail: "test@example.invalid", revenueId: 123, status: "COMPLETE" };
|
||||
assert.equal(await app.opportunityWorkloadActions.applySavedAction("A", "123", action), true);
|
||||
assert.equal(app.opportunityWorkloadActions.getSummary("A").state, "matched");
|
||||
assert.equal(app.opportunityWorkloadActions.getSummary("A").actions.length, 1);
|
||||
assert.equal(app.opportunityWorkloadActions.getSummary("B").actions.length, 0);
|
||||
assert.ok(app.opportunityWorkloadActions.createBadge("A").title.includes("ID: 77"));
|
||||
assert.deepEqual(writes[0].slice(0, 3), ["actions", "123", 1000]);
|
||||
assert.equal(writes[0][3][0].id, 77);
|
||||
await app.opportunityWorkloadActions.applySavedAction("A", "123", { action: { ...action, status: "OPEN" } });
|
||||
assert.equal(app.opportunityWorkloadActions.getSummary("A").actions.length, 1);
|
||||
assert.equal(app.opportunityWorkloadActions.getSummary("A").actions[0].status, "OPEN");
|
||||
await app.opportunityWorkloadActions.applySavedAction("A", "123", { actions: [{ ...action, id: 78, ownerEmail: "other@example.invalid" }] });
|
||||
assert.equal(app.opportunityWorkloadActions.getSummary("A").actions.length, 2);
|
||||
assert.equal(await app.opportunityWorkloadActions.applySavedAction("B", "123", action), false);
|
||||
assert.equal(await app.opportunityWorkloadActions.applySavedAction("A", "123", null), false);
|
||||
});
|
||||
|
||||
test("saved Action uses its email rather than display name for green badge and removes plus", async () => {
|
||||
const app = contentFixture(() => assert.fail("No follow-up GET"));
|
||||
app.activityCache.write = async () => {};
|
||||
app.productsContentState.opptyProductsByNumber.set("A", { items: [{ RevnId: "123" }] });
|
||||
const saved = { id: 90, owner: "Test User", ownerEmail: " TEST@example.invalid ", name: "REVIEW" };
|
||||
await app.opportunityWorkloadActions.applySavedAction("A", "123", saved);
|
||||
const badge = app.opportunityWorkloadActions.createBadge("A");
|
||||
assert.equal(badge.attributes["data-state"], "matched");
|
||||
assert.equal(badge.textContent, "1");
|
||||
await app.opportunityWorkloadActions.applySavedAction("A", "123", { id: 90, owner: "Test User" }, { ownerEmail: "test@example.invalid" });
|
||||
assert.equal(app.opportunityWorkloadActions.getSummary("A").state, "matched");
|
||||
app.authSessionState.principal = "another@example.invalid";
|
||||
assert.equal(app.opportunityWorkloadActions.getSummary("A").state, "missing");
|
||||
});
|
||||
|
||||
test("Actions completion includes queued calls, shared pending records, failures and cache writes", async () => {
|
||||
const pending = [];
|
||||
let finishWrite;
|
||||
|
||||
Reference in New Issue
Block a user