Replaced labels with icons in the SR and Action badges

This commit is contained in:
2026-09-27 19:18:24 -03:00
parent de4da0eec6
commit 1a3decbeb6
37 changed files with 1856 additions and 279 deletions

View File

@@ -14,7 +14,9 @@ function setup(family, saved = {}, principal = "user@example.invalid") {
remove: (keys, done) => local.remove(keys).then(done)
} } }
};
const app = loadApplication("content", { ...api, document: { createElement: () => ({ setAttribute() {}, addEventListener() {} }) } });
const document = { createElement: () => ({ append() {}, setAttribute() {}, addEventListener() {} }) };
document.createElementNS = (_, tag) => document.createElement(tag);
const app = loadApplication("content", { ...api, document });
app.authSessionState.principal = principal;
app.currentUser.allowsCrmActivities = () => true;
app.preferencesState.opptyProductsCacheHours = 24;
@@ -59,7 +61,7 @@ for (const family of ["chrome", "firefox"]) {
const count = h.calls.length;
await h.load(currentStamp, true);
assert.equal(h.calls.length, count);
assert.equal(h.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "\u2705 SR (2h)");
assert.equal(h.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "2h");
h.app.opportunitiesTable.renderOpportunitiesTable = () => {};
h.app.activityFilters.toggle("sr", "under4");
assert.equal(h.app.activityFilters.matches({ OptyNumber: "OP1" }), true);
@@ -82,15 +84,15 @@ for (const family of ["chrome", "firefox"]) {
const restored = setup(family, h.saved);
await restored.load(stamp, true);
assert.deepEqual(restored.calls, []);
assert.equal(restored.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "\u2705 SR (4h)");
assert.equal(restored.app.opportunityWorkloadActions.createBadge("OP1").textContent, "\u2705 Act");
assert.equal(restored.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "4h");
assert.equal(restored.app.opportunityWorkloadActions.createBadge("OP1").textContent, "1");
await restored.load(stamp, false);
assert.deepEqual(restored.calls, [], "valid matching cache also deduplicates repeated fresh-product notifications");
const other = setup(family, h.saved, "other@example.invalid");
await other.load(stamp, true);
assert.deepEqual(other.calls, []);
assert.equal(other.app.opportunityCrmServiceRequests.createBadge("OP1"), null);
assert.equal(other.app.opportunityWorkloadActions.createBadge("OP1").textContent, "Act ?");
assert.equal(other.app.opportunityWorkloadActions.createBadge("OP1").textContent, "?");
await restored.load(stamp + 1, false);
assert.deepEqual(restored.calls, ["sr", "actions"]);
});
@@ -133,7 +135,7 @@ test("storage failure does not lose live activity results and failures are never
h.local.get = async () => { throw new Error("Synthetic storage read failure"); };
h.local.set = async () => { throw new Error("Synthetic storage write failure"); };
await h.load(Date.now() - 1000, false);
assert.equal(h.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "\u2705 SR (4h)");
assert.equal(h.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "4h");
const failed = setup("firefox");
failed.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => { throw new Error("Synthetic failure"); };
failed.app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: false });

View File

@@ -11,7 +11,7 @@ class Element {
addEventListener(key, handler) { this.listeners[key] = handler; }
}
function setup() {
const app = loadApplication("content", { document: { createElement: tag => new Element(tag) } });
const app = loadApplication("content", { document: { createElement: tag => new Element(tag), createElementNS: (_, tag) => new Element(tag) } });
app.currentUser.allowsCrmActivities = () => true;
let renders = 0;
app.opportunitiesTable.renderOpportunitiesTable = () => { renders += 1; };
@@ -34,9 +34,28 @@ test("activity badge filters precede Status, are accessible and combine without
assert.equal(h.controls.children[0].children[1].attributes["aria-label"], "Filter by Service Request");
assert.equal(h.controls.children[3], h.stage);
assert.equal(h.form.attributes["data-activity-filters"], "true");
assert.equal(h.button("sr", "matched").textContent, "\u2705 SR");
assert.equal(h.button("actions", "missing").textContent, "\u26d4 Action");
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");
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"));
assert.ok(h.button("sr", value).title.startsWith("Whether a Service Request has been created for the opportunities.\n"));
for (const kind of ["sr", "actions"]) {
const button = h.button(kind, value);
assert.ok(button.title.endsWith(button.attributes["aria-label"]));
assert.equal(button.children.length, 1);
assert.equal(button.children[0].tag, "svg");
assert.equal(button.children[0].attributes["data-badge-icon"], kind);
assert.equal(button.children[0].attributes.stroke, "currentColor");
assert.equal(button.children[0].attributes["aria-hidden"], "true");
}
}
assert.equal(h.button("sr", "under4").textContent, "< 4h");
assert.equal(h.button("sr", "under4").children[0].attributes["data-badge-icon"], "sr");
assert.notEqual(h.button("actions", "matched").attributes["aria-label"], h.button("actions", "missing").attributes["aria-label"]);
assert.notEqual(h.button("sr", "matched").attributes["aria-label"], h.button("sr", "missing").attributes["aria-label"]);
h.sr.set("A", { state: "matched" });
h.actions.set("A", { state: "missing" });
h.sr.set("B", { state: "missing" });
@@ -74,6 +93,30 @@ test("SR below four hours is exclusive and regular SR options can replace it", (
assert.equal(h.matches("A"), false);
});
test("filter vectors retain their fixed slot without duplication when selection changes", () => {
const h = setup();
const options = [["sr", "matched"], ["sr", "under4"], ["sr", "missing"], ["actions", "matched"], ["actions", "missing"]];
const icons = options.map(([kind, value]) => h.button(kind, value).children[0]);
for (const [kind, value] of options) h.click(kind, value);
h.app.activityFilters.render();
options.forEach(([kind, value], index) => {
const button = h.button(kind, value);
assert.deepEqual(button.children, [icons[index]]);
assert.equal(icons[index].attributes.viewBox, "0 0 24 24");
assert.equal(icons[index].attributes.width, "16");
assert.equal(icons[index].attributes.height, "16");
assert.equal(icons[index].attributes["stroke-width"], "2.25");
assert.equal(icons[index].attributes.focusable, "false");
});
const css = fs.readFileSync(path.join(__dirname, "../src/features/opportunities/styles.css"), "utf8");
const shared = css.match(/\.opportunities-extension-cell-actions \.opportunities-extension-badge-icon,\s*\.opportunities-extension-activity-filter-button \.opportunities-extension-badge-icon \{([^}]+)\}/);
assert.ok(shared, "filters and opportunity badges share the same vector positioning");
for (const rule of ["order: -1;", "flex: 0 0 16px;", "width: 16px;", "height: 16px;", "stroke: currentColor;"]) {
assert.ok(shared[1].includes(rule), rule);
}
assert.match(css, /activity-filter-button \{\s*min-width: 32px;\s*gap: 4px;/);
});
test("missing filters exclude unknown, loading and error states; SALES_REP bypasses and hides them", () => {
const h = setup();
h.click("sr", "matched");
@@ -98,7 +141,7 @@ test("missing filters exclude unknown, loading and error states; SALES_REP bypas
for (const [roles, allowed] of [[ ["SALES_REP"], false ], [ ["SIMPLE_USER", "SALES_REP"], false ], [ ["SOLUTION_ENG_NOTERR"], true ]]) {
test(`requestCurrentUser roles ${JSON.stringify(roles)} control actual filter visibility`, async () => {
const app = loadApplication("content", { document: { createElement: tag => new Element(tag) } });
const app = loadApplication("content", { document: { createElement: tag => new Element(tag), createElementNS: (_, tag) => new Element(tag) } });
const calls = [];
let finish;
app.platformContentMessages.sendRuntimeMessage = message => {
@@ -146,7 +189,7 @@ test("SR filter summary uses actual records: zero, decimals, exact four, missing
});
test("Action filter summary shares badge ownership and uncertainty rules", async () => {
const app = loadApplication("content", { document: { createElement: tag => new Element(tag) } });
const app = loadApplication("content", { document: { createElement: tag => new Element(tag), createElementNS: (_, tag) => new Element(tag) } });
app.currentUser.allowsCrmActivities = () => true;
app.productsContentJobs.queueProductsTableRender = () => {};
app.authSessionState.principal = "me@example.invalid";
@@ -190,6 +233,16 @@ test("activity filters have theme contrast, visible keyboard focus and responsiv
assert.match(css, /badge-filters \{\s*display: flex;[\s\S]*?flex-wrap: wrap;/);
});
test("SR and Act filters share sizing and typography with Status and Stage", () => {
const css = fs.readFileSync(path.join(__dirname, "../src/features/opportunities/styles.css"), "utf8");
const shared = css.match(/\.opportunities-extension-form \.opportunities-extension-stage-filter-button,\s*\.opportunities-extension-form \.opportunities-extension-status-filter-button,\s*\.opportunities-extension-form \.opportunities-extension-activity-filter-button\s*\{([^}]+)\}/);
assert.ok(shared, "all filter badges use the same presentation rule");
for (const declaration of ["display: inline-flex;", "align-items: center;", "box-sizing: border-box;", "height: 28px;",
"padding: 4px 7px;", "border-radius: 18px;", "font-size: 11px;", "font-weight: 700;", "line-height: 18px;"]) {
assert.ok(shared[1].includes(declaration), declaration);
}
});
test("desktop keeps fields and badge groups on one line, wrapping only below the compact breakpoint", () => {
const css = fs.readFileSync(path.join(__dirname, "../src/features/opportunities/styles.css"), "utf8");
assert.match(css, /\.opportunities-extension-badge-filters \{ display: contents; \}/);

View File

@@ -56,7 +56,9 @@ test("CRM reports real page counts and Actions reports queued and completed requ
pending[0]({ ok: true, actions: [], hasNext: false });
await new Promise(resolve => setImmediate(resolve));
assert.deepEqual(progress.at(-1), ["actions", "running", 1, 2]);
pending[1]({ ok: false, error: "Synthetic failure" });
await new Promise(resolve => setImmediate(resolve));
for (let attempt = 1; attempt <= 5; attempt += 1) {
pending[attempt]({ ok: false, error: "Synthetic failure" });
await new Promise(resolve => setImmediate(resolve));
}
assert.deepEqual(progress.at(-1), ["actions", "complete-with-errors", 2, 2]);
});

View File

@@ -0,0 +1,108 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
const { response } = require("./support/characterization.cjs");
function setup(fetch) {
const app = loadApplication("content", { fetch });
app.currentUser.allowsCrmActivities = () => true;
app.opportunitiesQueryState.periodRequestVersion = 1;
app.debugContentView.renderDebugPanel = () => {};
app.activityProgress.update = () => {};
app.productsContentJobs.renderProductsProgress = () => {};
app.productsContentJobs.queueProductsTableRender = () => {};
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } });
const actions = [];
app.platformContentMessages.sendRuntimeMessage = async message => {
actions.push(message);
return { ok: true, actions: [], hasNext: false };
};
async function cycle(id) {
app.productsContentState.currentProductsRequestId = id;
app.productsContentState.opptyProductsByNumber.set("A", { items: [{ RevnId: "123" }], cachedAt: Date.now(), fromCache: false });
app.productActivities.begin(id, "synthetic");
await app.productActivities.complete(id);
}
return { app, actions, cycle };
}
test("SR succeeds on fifth attempt, is shared in flight and reused across Reference/product reloads", async () => {
let calls = 0;
const h = setup(async () => {
calls += 1;
if (calls < 5) throw new Error("Synthetic offline");
return response({ items: [], hasMore: false });
});
const first = h.app.opportunityCrmServiceRequests.load("synthetic", 1);
assert.equal(h.app.opportunityCrmServiceRequests.load("synthetic", 1), first);
assert.equal(await first, true);
assert.equal(calls, 5);
await h.cycle("current-quarter");
h.app.opportunitiesQueryState.periodRequestVersion += 1;
await h.cycle("next-quarter");
assert.equal(calls, 5);
assert.equal(h.actions.length, 2);
assert.equal(h.app.debugState.requestLog.filter(entry => entry.name === "resquestCrmServiceRequest").length, 5);
h.app.opportunityCrmServiceRequests.reset();
await h.cycle("explicit-refresh");
assert.equal(calls, 6);
});
for (const failure of [() => new Response("Denied", { status: 403 }), () => new Response("<html>Login</html>")]) {
test("SR exhausting five attempts blocks Actions and preserves the last completed timestamp", async () => {
let calls = 0;
const h = setup(async () => { calls += 1; return failure(); });
h.app.productsContentState.productsLastCachedAt = 123;
await h.cycle("failed");
assert.equal(calls, 5);
assert.equal(h.actions.length, 0);
assert.equal(h.app.opportunityCrmServiceRequests.getSummary("A").state, "error");
assert.equal(h.app.productsContentState.productsLastCachedAt, 123);
h.app.authSessionState.principal = "synthetic@example.invalid";
assert.equal(h.app.opportunityWorkloadActions.getSummary("A").state, "error");
});
}
test("pagination retries only the failed SR page, with no duplicate records", async () => {
const offsets = [];
let failures = 0;
const h = setup(async (_url, options) => {
const { offset } = JSON.parse(options.body);
offsets.push(offset);
if (offset === 1 && failures++ < 2) return response({ invalid: true });
return response({ items: [{ SrNumber: `SR${offset}`, ReportedHours_c: 2, OpportunityName_Id_c: { OptyNumber: "A" } }],
offset, count: 1, hasMore: offset === 0 });
});
await h.cycle("paged");
assert.deepEqual(offsets, [0, 1, 1, 1]);
assert.equal(h.app.opportunityCrmServiceRequests.getSummary("A").reportedHours, 4);
assert.equal(h.actions.length, 1);
});
test("Actions retries each workload up to five times and stops as soon as it succeeds", async () => {
const h = setup(async () => response({ items: [], hasMore: false }));
let attempts = 0;
h.app.platformContentMessages.sendRuntimeMessage = async () => {
attempts += 1;
return attempts < 3 ? { ok: false, error: "Synthetic" } : { ok: true, actions: [], hasNext: false };
};
await h.cycle("success");
assert.equal(attempts, 3);
attempts = 0;
h.app.platformContentMessages.sendRuntimeMessage = async () => { attempts += 1; throw new Error("Synthetic"); };
await h.cycle("failed-actions");
assert.equal(attempts, 5);
});
test("an invalidated SR request does not continue retrying or launch Actions", async () => {
let reject, calls = 0;
const h = setup(() => { calls += 1; return new Promise((_resolve, fail) => { reject = fail; }); });
const pending = h.cycle("old");
await new Promise(resolve => setImmediate(resolve));
h.app.opportunityCrmServiceRequests.reset();
h.app.productsContentState.currentProductsRequestId = "new";
reject(new Error("Synthetic"));
await pending;
assert.equal(calls, 1);
assert.equal(h.actions.length, 0);
});

View File

@@ -29,6 +29,7 @@ function setup(mockSalesToken = true) {
const context = {
document: {
createElement: (tag) => new Element(tag),
createElementNS: (_, tag) => new Element(tag),
querySelectorAll: () => visibleAlerts
},
fetch: (url, options) => new Promise((resolve, reject) => calls.push({ url, options, resolve, reject })),

View File

@@ -0,0 +1,68 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
function setup() {
const app = loadApplication("content");
const normal = { OptyNumber: "normal", Name: "Synthetic", ForecastGroup_c: "FORECAST", StatusCode: "OPEN",
CustomerAccount: { PartyId: "1", PartyUniqueName: "Customer" }, Owner: { PersonFirstName: "Owner" } };
const items = [normal,
{ ...normal, OptyNumber: "stage", ForecastGroup_c: null },
{ ...normal, OptyNumber: "status", StatusCode: "UNLISTED" },
{ ...normal, OptyNumber: "customer", CustomerAccount: null },
{ ...normal, OptyNumber: "owner", Owner: null }];
app.opportunitiesTableState.opportunitiesTableState.items = items;
app.opportunitiesFilters.resetCustomerFilter(items);
app.opportunitiesFilters.resetOwnerFilter(items);
return { app, items, result: () => Array.from(app.opportunitiesFilters.getSortedOpportunities(), item => item.OptyNumber) };
}
test("all selected includes records with missing or unlisted dimensions", () => {
const h = setup();
assert.equal(h.result().length, h.items.length);
h.app.opportunitiesFilterState.tableSearch = "normal";
assert.deepEqual(h.result(), ["normal"], "search still restricts results");
});
test("partial and empty selections remain restrictive, including WON_PENDING mapping", () => {
const h = setup();
const state = h.app.opportunitiesFilterState;
state.selectedStages.delete("SQL");
assert.equal(h.result().includes("stage"), false);
state.selectedStages.add("SQL");
state.selectedStatuses.delete("CLOSED");
assert.equal(h.result().includes("status"), false);
assert.equal(h.app.opportunitiesFilters.matchesSelectedStatus("WON_PENDING"), true);
state.selectedStatuses.clear();
assert.deepEqual(h.result(), []);
});
test("customer and owner exclusions apply only when a narrower selection is made", () => {
const h = setup();
const state = h.app.opportunitiesFilterState;
state.selectedCustomerKeys.clear();
assert.deepEqual(h.result(), []);
state.selectedCustomerKeys.add("party:1");
assert.equal(h.result().includes("customer"), true);
state.selectedOwnerKeys.clear();
assert.deepEqual(h.result(), []);
});
test("Owner offers one dash option for unassigned records and supports selecting it alone", () => {
const h = setup();
for (const Owner of [undefined, {}, "", " ", { PersonFirstName: " " }]) {
h.items.push({ ...h.items[0], OptyNumber: `missing-${h.items.length}`, Owner });
}
const options = Array.from(h.app.opportunitiesFormatters.getOwnerOptions());
const missing = options.filter(option => option.label === "-");
assert.equal(missing.length, 1);
assert.equal(missing[0].key, "owner:unassigned");
h.app.opportunitiesFilterState.selectedOwnerKeys = new Set([missing[0].key]);
assert.equal(h.result().length, 6);
assert.equal(h.result().includes("normal"), false);
h.app.opportunitiesFilterState.selectedOwnerKeys = new Set(options.map(option => option.key));
assert.equal(h.result().length, h.items.length);
h.app.opportunitiesFilterState.selectedOwnerKeys.delete(missing[0].key);
assert.equal(h.result().includes("owner"), false);
assert.equal(h.result().length, 4);
});

113
tests/badgeIcons.test.cjs Normal file
View File

@@ -0,0 +1,113 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const fs = require("node:fs");
const path = require("node:path");
const { loadApplication, composeScript } = require("./support/application.cjs");
const root = path.join(__dirname, "..");
class Element {
constructor(tag) {
this.tag = tag; this.children = []; this.attributes = {}; this.listeners = {}; this.text = "";
this.classList = { toggle() {} };
}
set textContent(value) { this.text = String(value); this.children = []; }
get textContent() { return this.text + this.children.map(child => child.textContent).join(""); }
append(...children) { this.children.push(...children); }
setAttribute(key, value) { this.attributes[key] = value; }
addEventListener(key, callback) { this.listeners[key] = callback; }
}
function setup() {
const app = loadApplication("content", { document: { createElement: tag => new Element(tag), getElementById: () => null,
createElementNS: (namespace, tag) => Object.assign(new Element(tag), { namespace }) } });
app.currentUser.allowsCrmActivities = () => true;
app.productsContentJobs.queueProductsTableRender = () => {};
app.activityProgress.update = () => {};
app.authSessionState.principal = "me@example.invalid";
return app;
}
function checkIcon(badge, kind, value) {
assert.equal(badge.textContent, value);
const images = badge.children.filter(child => child.tag === "svg");
assert.equal(images.length, 1);
const icon = images[0];
assert.equal(icon.namespace, "http://www.w3.org/2000/svg");
assert.equal(icon.attributes.viewBox, "0 0 24 24");
assert.equal(icon.attributes.fill, "none");
assert.equal(icon.attributes.stroke, "currentColor");
assert.equal(icon.attributes["stroke-width"], "2.25");
assert.equal(icon.attributes["stroke-linecap"], "round");
assert.equal(icon.attributes["stroke-linejoin"], "round");
assert.equal(icon.attributes["data-badge-icon"], kind);
assert.equal(icon.attributes["aria-hidden"], "true");
assert.equal(icon.attributes.focusable, "false");
assert.equal(icon.attributes.width, "16");
assert.equal(icon.attributes.height, "16");
assert.ok(icon.children.length >= 3);
assert.ok(icon.children.every(child => ["path", "rect", "polyline"].includes(child.tag)));
assert.ok(icon.children.every(child => child.namespace === icon.namespace));
assert.equal(badge.children.some(child => child.tag === "img"), false);
}
test("consistent inline vectors replace raster icons without parentheses or lost tooltips", async () => {
const app = setup();
const products = { items: [1, 2, 3].map(RampMonths_c => ({ RampMonths_c, RevnId: "123" })) };
const count = app.productsDetails.createProductsCountButton("TEST", products);
checkIcon(count, "products", "3");
assert.equal(count.title, "3 products");
assert.equal(typeof count.listeners.click, "function");
const ramp = app.productsDetails.createProductsRampBadge(products);
checkIcon(ramp, "ramp", "1,2,3");
assert.equal(ramp.title, "Ramp Months: 1, 2, 3");
const missingRamp = app.productsDetails.createProductsRampBadge({ items: [{ RampMonths_c: null }] });
checkIcon(missingRamp, "ramp", "N/D");
assert.equal(missingRamp.attributes["data-missing"], "true");
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [
{ SrNumber: "SR-TEST", ReportedHours_c: 4, OpportunityName_Id_c: { OptyNumber: "TEST" } }
];
await app.opportunityCrmServiceRequests.load("synthetic", app.opportunitiesQueryState.periodRequestVersion);
const sr = app.opportunityCrmServiceRequests.createBadge("TEST");
checkIcon(sr, "sr", "4h");
assert.equal(sr.title, "Whether a Service Request has been created for the opportunities.");
assert.equal(typeof sr.listeners.click, "function");
checkIcon(app.opportunityCrmServiceRequests.createBadge("OTHER"), "sr", "0h");
app.productsContentState.opptyProductsByNumber.set("TEST", products);
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } });
app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: true, hasNext: false, actions: [
{ id: 1, owner: "me@example.invalid", name: "My action" },
{ id: 2, owner: "other@example.invalid", name: "Other action" }
] });
await app.opportunityWorkloadActions.loadProducts(products);
const actions = app.opportunityWorkloadActions.createBadge("TEST");
checkIcon(actions, "actions", "2");
assert.equal(actions.attributes["data-state"], "matched");
assert.match(actions.title, /My action/);
assert.match(actions.title, /Other action/);
app.currentUser.allowsCrmActivities = () => false;
assert.equal(app.opportunityWorkloadActions.createBadge("TEST"), null);
assert.equal(app.opportunityCrmServiceRequests.createBadge("TEST"), null);
});
test("vector shapes and their license ship in both distributions without raster images or requests", () => {
const source = composeScript("content");
const module = fs.readFileSync(path.join(root, "src/features/opportunities/badge-content.js"), "utf8");
assert.ok(source.includes(module));
assert.doesNotMatch(source, /opportunityBadgeImages|data:image\/png;base64/);
for (const target of ["chromium", "firefox"]) {
const bundle = fs.readFileSync(path.join(root, "dist", target, "content.js"), "utf8");
assert.ok(bundle.includes(module));
assert.ok(bundle.includes("Copyright (c) 2026 Lucide Icons and Contributors"));
}
});
test("icon styles preserve uniform dimensions and provide light, dark and active contrast", () => {
const css = fs.readFileSync(path.join(root, "src/features/opportunities/styles.css"), "utf8");
const icon = css.match(/\.opportunities-extension-badge-icon \{([^}]+)\}/)[1];
for (const rule of ["order: -1;", "flex: 0 0 16px;", "width: 16px;", "height: 16px;", "color: inherit;", "stroke: currentColor;", "stroke-width: 2.25;"]) {
assert.ok(icon.includes(rule), rule);
}
assert.doesNotMatch(icon, /filter:|mix-blend-mode:/);
assert.match(css, /gap: 4px;\s*height: 20px;/);
assert.match(css, /cell-actions > \.opportunities-extension-crm-sr-badge \{\s*display: inline-flex;\s*align-items: center;\s*justify-content: flex-start;/);
});

View File

@@ -0,0 +1,73 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadBackground, response } = require("./support/characterization.cjs");
const tokens = [
{ tokenService: "ACCP-TS", uiaasHeader: { value: "synthetic-account" } },
{ tokenService: "OCICONS-TS", uiaasHeader: { value: "synthetic-consumption" } },
{ tokenService: "WWB-TS", uiaasHeader: { value: "synthetic-actions" } }
];
const request = { type: "opportunitiesExtension.requestConsumerTokenService" };
for (const family of ["chrome", "firefox"]) {
for (const status of [401, 403]) {
test(`${family}: HTTP ${status} opens an inactive SPA session and closes it after valid JSON`, async () => {
let attempts = 0;
const h = loadBackground({ family, fetch: async (_url, options) => {
attempts += 1;
assert.equal(options.credentials, "include");
if (attempts === 1) return response({ error: "Session unavailable" }, status);
assert.equal(h.tabs.length, 1, "session tab must open before retrying");
return response(tokens);
} });
const result = await h.dispatch(request);
assert.equal(result.ok, true);
assert.equal(attempts, 2);
assert.equal(h.tabs.length, 1);
assert.equal(h.tabs[0].active, false);
assert.equal(h.tabs[0].windowId, 3);
assert.equal(h.tabs[0].cookieStoreId, family === "firefox" ? "test-store" : undefined);
assert.equal(h.tabs[0].url, "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html");
assert.deepEqual(h.removedTabs, [91]);
assert.equal(result.debugRequests[0].httpStatus, status);
assert.match(result.debugRequests[0].metadata.retryReason, /SPA session recovery/);
assert.equal(result.debugRequests[1].metadata.sessionBootstrapState, "closed-after-valid-json");
assert.equal(result.authorizations.workloadActions.value, "synthetic-actions");
});
}
test(`${family}: persistent 403 stops after five attempts and removes the auxiliary tab`, async () => {
let attempts = 0;
const h = loadBackground({ family, fetch: async () => { attempts += 1; return response({}, 403); } });
const result = await h.dispatch(request);
assert.equal(result.ok, false);
assert.equal(attempts, 5);
assert.equal(h.tabs.length, 1);
assert.deepEqual(h.removedTabs, [91]);
assert.match(result.error, /403 after 5 attempts/);
assert.equal(result.debugRequests.at(-1).metadata.retryLimitReached, "true");
assert.equal(result.debugRequests.at(-1).metadata.sessionBootstrapState, "closed-after-retry-finished");
});
}
test("mixed 403/HTML recovery shares one tab and the existing retry budget", async () => {
let attempts = 0;
const h = loadBackground({ fetch: async () => {
attempts += 1;
if (attempts === 1) return response({}, 403);
if (attempts === 2) return new Response("<html>Login</html>");
return response(tokens);
} });
const result = await h.dispatch(request);
assert.equal(result.ok, true);
assert.equal(attempts, 3);
assert.equal(h.tabs.length, 1);
assert.deepEqual(h.removedTabs, [91]);
assert.equal(result.debugRequests[1].metadata.sessionBootstrapState, "waiting-for-valid-session");
});
test("unrelated HTTP failures do not launch the SPA session page", async () => {
const h = loadBackground({ fetch: async () => response({}, 500) });
const result = await h.dispatch(request);
assert.equal(result.ok, false);
assert.equal(result.debugRequests.length, 1);
assert.equal(h.tabs.length, 0);
});

View File

@@ -3,7 +3,8 @@ const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
const response = data => new Response(JSON.stringify(data), { status: 200 });
function setup(fetch) {
const document = { createElement: () => ({ attributes: {}, addEventListener() {}, setAttribute(key, value) { this.attributes[key] = value; } }) };
const document = { createElement: () => ({ attributes: {}, append() {}, addEventListener() {}, setAttribute(key, value) { this.attributes[key] = value; } }) };
document.createElementNS = (_, tag) => document.createElement(tag);
const app = loadApplication("content", { fetch, document, Intl, Date });
app.currentUser.allowsCrmActivities = () => true;
app.debugContentView.renderDebugPanel = () => {};
@@ -18,8 +19,12 @@ test("SR badge shows reported hours, sums linked SRs and preserves missing value
for (const [values, expected] of [[[4], "4h"], [[0], "0h"], [[2, "1.5"], "3.5h"], [[0.1, 0.2], "0.3h"], [[null], "N/D"], [[4, null], "N/D"], [[""], "N/D"], [["invalid"], "N/D"]]) {
const app = setup(async () => response({ items: values.map((value, index) => ({ ...sr("A", `SR-${index}`), ReportedHours_c: value })), hasMore: false }));
await app.opportunityCrmServiceRequests.load("synthetic", 1);
assert.equal(app.opportunityCrmServiceRequests.createBadge("A").textContent, `\u2705 SR (${expected})`);
assert.equal(app.opportunityCrmServiceRequests.createBadge("OTHER").textContent, "\u26d4 SR");
assert.equal(app.opportunityCrmServiceRequests.createBadge("A").textContent, expected);
assert.equal(app.opportunityCrmServiceRequests.createBadge("OTHER").textContent, "0h");
assert.equal(app.opportunityCrmServiceRequests.createBadge("A").title, "Whether a Service Request has been created for the opportunities.");
assert.equal(app.opportunityCrmServiceRequests.createBadge("OTHER").title, "Whether a Service Request has been created for the opportunities.");
assert.equal(app.opportunityCrmServiceRequests.createBadge("A").attributes["data-state"], "matched");
assert.equal(app.opportunityCrmServiceRequests.createBadge("OTHER").attributes["data-state"], "missing");
}
});
@@ -89,11 +94,11 @@ test("CRM query preserves payload and pagination after opportunities, uses token
assert.equal(app.debugContentView.getRequestCategory(log), "serviceRequests");
}
const found = app.opportunityCrmServiceRequests.createBadge("TEST-1");
assert.equal(found.textContent, "\u2705 SR (0h)");
assert.equal(found.textContent, "0h");
assert.equal(found.attributes["data-state"], "matched");
for (const detail of ["SR-0", "SR-1", "Reported Hours: 0", "Test Owner", "test@example.invalid", "<script>synthetic</script>"])
assert.ok(found.attributes["aria-label"].includes(detail));
assert.equal(app.opportunityCrmServiceRequests.createBadge("OTHER").textContent, "\u26d4 SR");
assert.equal(app.opportunityCrmServiceRequests.createBadge("OTHER").textContent, "0h");
assert.equal(app.opportunityCrmServiceRequests.createBadge(null), null);
app.opportunityCrmServiceRequests.reset();
assert.equal(app.opportunityCrmServiceRequests.createBadge("TEST-1"), null);
@@ -118,7 +123,7 @@ test("CRM failures do not mislabel missing SRs or block the opportunity list", a
await app.opportunitiesController.requestOpportunities("token", "Current Quarter", {}, 1);
assert.equal(loaded, true);
await app.opportunityCrmServiceRequests.load("token", 1);
assert.equal(app.opportunityCrmServiceRequests.createBadge("TEST").textContent, "SR ?");
assert.equal(app.opportunityCrmServiceRequests.createBadge("TEST").textContent, "?");
assert.equal(app.debugState.requestLog.at(-1).status, "failed");
}
});
@@ -133,8 +138,8 @@ test("stale CRM results cannot overwrite a reopened modal or continue pagination
await app.opportunityCrmServiceRequests.load("token", 2);
resolveOld(response({ items: [sr("OLD")], hasMore: true, count: 1, offset: 0 }));
await old;
assert.equal(app.opportunityCrmServiceRequests.createBadge("NEW").textContent, "\u2705 SR (0h)");
assert.equal(app.opportunityCrmServiceRequests.createBadge("OLD").textContent, "\u26d4 SR");
assert.equal(app.opportunityCrmServiceRequests.createBadge("NEW").textContent, "0h");
assert.equal(app.opportunityCrmServiceRequests.createBadge("OLD").textContent, "0h");
assert.equal(app.debugState.requestLog.length, 1);
});
@@ -155,8 +160,8 @@ test("Reference changes share pending pagination and reuse completed CRM results
app.opportunitiesQueryState.periodRequestVersion = 3;
await app.opportunityCrmServiceRequests.load("token", 3);
assert.equal(calls, 2);
assert.equal(app.opportunityCrmServiceRequests.createBadge("A").textContent, "\u2705 SR (0h)");
assert.equal(app.opportunityCrmServiceRequests.createBadge("B").textContent, "\u2705 SR (0h)");
assert.equal(app.opportunityCrmServiceRequests.createBadge("A").textContent, "0h");
assert.equal(app.opportunityCrmServiceRequests.createBadge("B").textContent, "0h");
app.opportunityCrmServiceRequests.reset();
await app.opportunityCrmServiceRequests.load("token", 3);
assert.equal(calls, 3);

View File

@@ -0,0 +1,48 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
class Element {
constructor() { this.children = []; this.attributes = {}; }
set textContent(value) { this.text = value; this.children = []; }
get textContent() { return this.text; }
append(...nodes) { this.children.push(...nodes); }
setAttribute(key, value) { this.attributes[key] = value; }
addEventListener() {}
}
test("Total Opportunities displays filtered/overall and updates with filters and Reference data", () => {
const dashboard = new Element(), table = new Element();
const app = loadApplication("content", {
document: {
createElement: () => new Element(),
getElementById: id => id === "opportunities-extension-stage-dashboard" ? dashboard
: id === "opportunities-extension-table" ? table : null
},
performance: { now: () => 0 }, requestAnimationFrame: tick => tick(480)
});
// Use real table/dashboard coordination without constructing unrelated row controls.
app.opportunitiesTableState.opportunitiesTableState.status = "loading";
const items = Array.from({ length: 26 }, () => ({ ForecastGroup_c: "FORECAST" }));
app.opportunitiesTableState.opportunitiesTableState.items = items;
let filtered = items.slice(0, 11);
app.opportunitiesFilters.getSortedOpportunities = () => filtered;
const value = () => dashboard.children[0].children[1];
app.opportunitiesTable.renderOpportunitiesTable();
assert.equal(value().textContent, "11/26");
assert.equal(value().attributes["aria-label"], "11 filtered opportunities out of 26 total");
filtered = [];
app.opportunitiesTable.renderOpportunitiesTable();
assert.equal(value().textContent, "0/26");
filtered = items;
app.opportunitiesTable.renderOpportunitiesTable();
assert.equal(value().textContent, "26/26");
app.opportunitiesTableState.opportunitiesTableState.items = items.slice(0, 3);
filtered = items.slice(0, 2);
app.opportunitiesTable.renderOpportunitiesTable();
assert.equal(value().textContent, "2/3");
app.opportunitiesTableState.opportunitiesTableState.items = [];
filtered = [];
app.opportunitiesTable.renderOpportunitiesTable();
assert.equal(value().textContent, "0/0");
});

View File

@@ -59,6 +59,7 @@ test("ten columns retain their links and put product/customer controls below the
assert.ok(name.children[0].href.endsWith("TEST-1"));
assert.equal(name.children[1].children[0].className, "opportunities-extension-products-count");
assert.equal(name.children[1].children[0].textContent, "1");
assert.equal(name.children[1].children[0].title, "1 product");
assert.equal(customer.children[0].textContent, "Synthetic customer");
assert.ok(customer.children[0].href.endsWith("party%2Fid"));
assert.equal(customer.children[1].children[0].title, "View customer consumption");
@@ -82,8 +83,8 @@ test("ramp months preserve zero and flag missing values beside the product count
const badge = () => findClass(h.cell("name"), "opportunities-extension-products-ramp");
assert.equal(badge(), undefined);
for (const [values, expected] of [
[[], null], [[null], "RM N/D"], [[null, undefined, "", " "], "RM N/D"], [[6], "RM 6"],
[[0], "RM 0"], [[0, "3", null, " 6 ", 3], "RM 0,3,N/D,6,3"]
[[], null], [[null], "N/D"], [[null, undefined, "", " "], "N/D"], [[6], "6"],
[[0], "0"], [[0, "3", null, " 6 ", 3], "0,3,N/D,6,3"]
]) {
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", {
fromCache: true, items: values.map(RampMonths_c => ({ RampMonths_c }))
@@ -93,10 +94,13 @@ test("ramp months preserve zero and flag missing values beside the product count
else {
assert.equal(badge().textContent, expected);
assert.equal(badge().attributes["data-missing"], String(expected.includes("N/D")));
assert.match(badge().title, /^Ramp Months:/);
assert.equal(badge().title, `Ramp Months: ${expected.split(",").join(", ")}`);
assert.equal(badge().attributes["aria-label"], badge().title);
assert.equal(h.cell("name").children[0].children[1].children[1], badge());
}
assert.equal(findClass(h.cell("name"), "opportunities-extension-products-count").textContent, String(values.length));
const count = findClass(h.cell("name"), "opportunities-extension-products-count");
assert.equal(count.textContent, String(values.length));
assert.equal(count.title, `${values.length} product${values.length === 1 ? "" : "s"}`);
}
});
@@ -118,6 +122,33 @@ test("products remain expandable with AI summary and a ten-column detail row aft
assert.deepEqual(h.summaries, ["B"], "collapsing does not generate another summary");
});
test("product labels support multiple digits without changing their tooltip", () => {
const h = setup();
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", { items: Array.from({ length: 12 }, () => ({})) });
h.app.opportunitiesTable.renderOpportunitiesTable();
const count = findClass(h.cell("name"), "opportunities-extension-products-count");
assert.equal(count.textContent, "12");
assert.equal(count.title, "12 products");
assert.equal(count.attributes["aria-label"], "12 products for opportunity TEST-1");
});
test("opportunity badges share height, typography and alignment including the Action plus button", () => {
const css = fs.readFileSync(path.join(__dirname, "../src/features/opportunities/styles.css"), "utf8");
const shared = css.match(/\.opportunities-extension-cell-actions > \.opportunities-extension-products-count,\s*\.opportunities-extension-cell-actions > \.opportunities-extension-products-ramp,\s*\.opportunities-extension-cell-actions > \.opportunities-extension-crm-sr-badge\s*\{([^}]+)\}/);
assert.ok(shared, "P, R, SR and Act share one scoped badge rule");
for (const declaration of ["display: inline-flex;", "align-items: center;", "justify-content: flex-start;",
"box-sizing: border-box;", "height: 20px;", "min-height: 20px;", "padding: 0 6px;", "margin: 0;",
"border-radius: 8px;", "font-family: inherit;", "font-size: 10px;", "font-weight: 600;", "line-height: 16px;"]) {
assert.ok(shared[1].includes(declaration), declaration);
}
const plus = css.match(/\.opportunities-extension-add-action\s*\{([^}]+)\}/)[1];
assert.match(plus, /height: 16px;/);
assert.match(plus, /align-items: center;/);
assert.match(plus, /font: inherit;/);
const actions = fs.readFileSync(path.join(__dirname, "../src/features/opportunities/workload-actions.js"), "utf8");
assert.match(actions, /badge.className = "opportunities-extension-crm-sr-badge opportunities-extension-wl-actions-badge"/);
});
test("CRM SR badge follows Ramp Months and remains visible while product data is pending", async () => {
const h = setup();
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [{
@@ -125,11 +156,11 @@ test("CRM SR badge follows Ramp Months and remains visible while product data is
}];
await h.app.opportunityCrmServiceRequests.load("synthetic", h.app.opportunitiesQueryState.periodRequestVersion);
h.app.opportunitiesTable.renderOpportunitiesTable();
assert.equal(findClass(h.cell("name"), "opportunities-extension-crm-sr-badge").textContent, "\u2705 SR (0h)");
assert.equal(findClass(h.cell("name"), "opportunities-extension-crm-sr-badge").textContent, "0h");
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", { items: [{ RampMonths_c: 3 }] });
h.app.opportunitiesTable.renderOpportunitiesTable();
const actions = h.cell("name").children[0].children[1];
assert.deepEqual(actions.children.map(child => child.textContent), ["1", "RM 3", "\u2705 SR (0h)", "Act ?"]);
assert.deepEqual(actions.children.map(child => child.textContent), ["1", "3", "0h", "?"]);
findClass(h.cell("name"), "opportunities-extension-products-count").click();
assert.ok(findClass(h.rows()[1], "opportunities-extension-products-table"));
});

View File

@@ -48,7 +48,8 @@ test("Last update ignores superseded cycles and settles failures without changin
assert.equal(h.app.productsContentState.productsLastCachedAt, 100);
current.reject(new Error("Synthetic failure"));
await flush();
assert.equal(h.app.productsContentState.productsLastCachedAt, 300);
assert.equal(h.app.productsContentState.productsLastCachedAt, 100);
assert.equal(h.calls.includes("empty products"), false, "SR failure blocks Actions");
});
test("Last update waits for cache restoration and ignores a superseded Reference", async () => {
@@ -111,7 +112,7 @@ test("follow-up calls wait for completion, deduplicate terminal messages and ref
h.progress(first, { state: "complete" });
h.progress(first, { state: "complete" });
await flush();
assert.deepEqual(h.calls, ["reset SR", "123", "SR token"]);
assert.deepEqual(h.calls, ["SR token", "123"]);
const second = h.start("new-token");
h.progress(first, { state: "complete", optyNumber: "B", items: [{ RevnId: "old" }] });
h.progress(second, { state: "running", optyNumber: "A", items: [{ RevnId: "123" }] });
@@ -152,13 +153,13 @@ test("cached products restore activities without network; mixed cycles fetch onl
h.progress(cached, { optyNumber: "A", items: [{ RevnId: "cached" }], fromCache: true });
h.progress(cached, { state: "complete" });
await flush();
assert.deepEqual(h.calls, ["reset SR"]);
assert.deepEqual(h.calls, []);
const mixed = h.start();
h.progress(mixed, { optyNumber: "A", items: [{ RevnId: "cached" }], fromCache: true });
h.progress(mixed, { optyNumber: "B", items: [{ RevnId: "fresh" }], fromCache: false });
h.progress(mixed, { state: "complete" });
await flush();
assert.deepEqual(h.calls, ["reset SR", "reset SR", "fresh", "SR token"]);
assert.deepEqual(h.calls, ["SR token", "fresh"]);
});
test("reopening an existing modal does not reload products or reset/query activities", async () => {

View File

@@ -0,0 +1,115 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
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 on the missing Action badge opens a form with six fields and no 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), ["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.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: "<script>synthetic</script>", 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", "<script>synthetic</script>", "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", "-"]);
});

View File

@@ -52,7 +52,9 @@ test("workload actions reject missing credentials, invalid ids, HTTP/network/JSO
});
function contentFixture(send) {
const app = loadApplication("content", { document: { createElement: () => ({ attributes: {}, setAttribute(k, v) { this.attributes[k] = v; } }) } });
const document = { createElement: () => ({ attributes: {}, append() {}, addEventListener() {}, setAttribute(k, v) { this.attributes[k] = v; } }) };
document.createElementNS = (_, tag) => document.createElement(tag);
const app = loadApplication("content", { document });
app.currentUser.allowsCrmActivities = () => true;
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic-wwb" } });
app.authSessionState.principal = "test@example.invalid";
@@ -74,12 +76,14 @@ test("Actions completion includes queued calls, shared pending records, failures
const shared = app.opportunityWorkloadActions.loadProducts(products).then(() => { sharedComplete = true; });
await flush();
assert.equal(pending.length, 4);
pending.slice(0, 4).forEach(resolve => resolve({ ok: false, error: "Synthetic" }));
await flush();
assert.equal(pending.length, 5);
for (let attempt = 0; attempt < 5; attempt += 1) {
pending.slice(attempt * 4, attempt * 4 + 4).forEach(resolve => resolve({ ok: false, error: "Synthetic" }));
await flush();
}
assert.equal(pending.length, 21);
assert.equal(complete, false);
assert.equal(sharedComplete, false);
pending[4]({ ok: true, actions: [], hasNext: false });
pending[20]({ ok: true, actions: [], hasNext: false });
await flush();
assert.equal(complete, false, "cache write still pending");
finishWrite();
@@ -109,18 +113,18 @@ test("actions aggregate all products, share RevnId queries across Reference chan
const products = { items: [{ RevnId: "1" }, { RevnId: 2 }, { RevnId: "1" }] };
app.productsContentState.opptyProductsByNumber.set("A", products);
app.opportunityWorkloadActions.loadProducts(products);
assert.equal(app.opportunityWorkloadActions.createBadge("A").textContent, "Act ...");
assert.equal(app.opportunityWorkloadActions.createBadge("A").textContent, "...");
await flush();
assert.equal(calls.length, 2);
assert.equal(calls[0].authorization.value, "synthetic-wwb");
const badge = app.opportunityWorkloadActions.createBadge("A");
assert.equal(badge.textContent, "\u2705 Act");
assert.equal(badge.textContent, "1");
assert.match(badge.title, /<script>safe<\/script>/);
app.opportunityWorkloadActions.loadProducts(products);
await flush();
assert.equal(calls.length, 2);
app.productsContentState.opptyProductsByNumber.set("B", { items: [{ RevnId: 2 }] });
assert.equal(app.opportunityWorkloadActions.createBadge("B").textContent, "\u26d4 Act");
assert.equal(app.opportunityWorkloadActions.createBadge("B").textContent, "0");
app.opportunityWorkloadActions.reset(false);
app.opportunityWorkloadActions.loadProducts(products);
await flush();
@@ -138,9 +142,9 @@ test("errors, missing ids and partial empty pages never appear as confirmed abse
app.productsContentState.opptyProductsByNumber.set("A", products);
app.opportunityWorkloadActions.loadProducts(products);
await flush();
assert.equal(app.opportunityWorkloadActions.createBadge("A").textContent, "Act ?");
assert.equal(app.opportunityWorkloadActions.createBadge("A").textContent, "?");
app.productsContentState.opptyProductsByNumber.set("A", { items: [{}] });
assert.equal(app.opportunityWorkloadActions.createBadge("A").textContent, "Act ?");
assert.equal(app.opportunityWorkloadActions.createBadge("A").textContent, "?");
}
});
@@ -155,15 +159,17 @@ test("Act requires an owner matching tokenrelay principal while tooltip retains
app.opportunityWorkloadActions.loadProducts(products);
await flush();
const badge = () => app.opportunityWorkloadActions.createBadge("A");
assert.equal(badge().textContent, "\u2705 Act");
assert.equal(badge().textContent, "2");
assert.equal(badge().attributes["data-state"], "matched");
assert.match(badge().title, /Other action/);
assert.match(badge().title, /My action/);
app.authSessionState.principal = "different@example.invalid";
assert.equal(badge().textContent, "\u26d4 Act", "ownerEmail must not determine ownership");
assert.equal(badge().textContent, "2");
assert.equal(badge().attributes["data-state"], "missing", "ownerEmail must not determine ownership");
assert.match(badge().title, /Other action/);
assert.match(badge().title, /My action/);
app.authSessionState.principal = "";
assert.equal(badge().textContent, "Act ?");
assert.equal(badge().textContent, "?");
});
test("general tokenrelay retains principal without changing access-token return or accepting sales-common identity", async () => {