Loading comcipapic to establish a session for SR requests

This commit is contained in:
2026-10-02 02:37:35 -03:00
parent 3fd89a307c
commit f512ef9928
27 changed files with 2377 additions and 373 deletions

View File

@@ -30,6 +30,13 @@ test("activity indicators share Products styles, fade after 5s, retain failures
assert.equal(timers.size, 0);
update("sr", "error");
assert.equal(nodes[0].textContent, "Service Requests unavailable");
update("session", "running");
assert.equal(nodes[2].textContent, "SR session loading");
assert.equal(nodes[2].attributes["aria-busy"], "true");
update("session", "complete");
assert.equal(nodes[2].textContent, "SR session ready");
assert.equal(nodes[2].attributes["aria-busy"], "false");
assert.equal([...timers.values()].at(-1).delay, 5000);
});
test("CRM reports real page counts and Actions reports queued and completed requests", async () => {

View File

@@ -70,7 +70,7 @@ test("consistent inline vectors replace raster icons without parentheses or lost
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");
checkIcon(app.opportunityCrmServiceRequests.createBadge("OTHER"), "sr", "0h+");
app.productsContentState.opptyProductsByNumber.set("TEST", products);
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } });

View File

@@ -2,6 +2,227 @@ const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadBackground, response } = require("./support/characterization.cjs");
const type = "opportunitiesExtension.requestInternalServiceRequest";
test("preparation reuses an existing unexpired cookie pair without navigation or API calls", async () => {
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
const h = loadBackground({
fetch: async () => assert.fail("No API calls during preparation"),
cookies: () => [1, 2].map(part => ({ name: `ORA_OCIS_CG_SESSION_idcs-existing_${domain}_${part}`, value: `secret-${part}`, domain }))
});
const result = await h.dispatch({ type: "opportunitiesExtension.prepareSrSession" });
assert.equal(result.ok, true);
assert.equal(h.tabs.length, 0);
assert.equal(h.removedTabs.length, 0);
});
test("preparation closes immediately on the second cookie event even while getAll is pending", { timeout: 2000 }, async () => {
const fs = require("node:fs"), vm = require("node:vm"), path = require("node:path");
const factory = vm.runInNewContext(`${fs.readFileSync(path.join(__dirname, "../src/services/fusion/sr-details.js"), "utf8")}\ncreateFusionSrDetails`);
const listeners = new Set();
let reads = 0, closed = 0;
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
const cookie = part => ({ name: `ORA_OCIS_CG_SESSION_idcs-new_${domain}_${part}`, value: `secret-${part}`, domain, storeId: "store" });
const emit = change => { for (const listener of listeners) listener(change); };
const api = factory({
fetchJson: async () => assert.fail("No API calls during preparation"),
cookieChanges: { addListener: fn => listeners.add(fn), removeListener: fn => listeners.delete(fn) },
cookiesGetAll: () => ++reads === 1 ? Promise.resolve([]) : new Promise(() => {}),
tabsCreate: async () => {
setImmediate(() => {
emit({ cookie: cookie(1) });
emit({ cookie: { ...cookie(2), storeId: "other" } });
assert.equal(closed, 0);
emit({ cookie: cookie(2), removed: true });
assert.equal(closed, 0);
emit({ cookie: cookie(2) });
});
return { id: 7 };
},
tabsRemove: async id => { assert.equal(id, 7); closed += 1; },
supportsFirefoxCookieStoreTabs: () => false,
wait: () => new Promise(() => {})
});
assert.equal((await api.prepareSession({ tab: { cookieStoreId: "store" } })).ok, true);
assert.equal(closed, 1);
assert.equal(listeners.size, 0);
});
test("explicit preparation opens SeaaS initiation and observes cookies without any API requests", async () => {
let calls = 0;
let reads = 0;
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
const pair = [1, 2].map(part => ({ name: `ORA_OCIS_CG_SESSION_idcs-new_${domain}_${part}`, value: `secret-${part}`, domain }));
const h = loadBackground({ fetch: async () => {
calls += 1;
assert.fail("Opening the main modal must not trigger any session-dependent API request");
}, cookies: () => {
reads += 1;
if (reads === 1) return [];
assert.equal(h.tabs.length, 1);
if (reads === 2) return pair.slice(0, 1);
if (reads === 3) return pair.map(cookie => ({ ...cookie, expirationDate: 1 }));
return pair;
} });
const result = await h.dispatch({ type: "opportunitiesExtension.prepareSrSession" });
assert.equal(result.ok, true);
assert.equal(calls, 0);
assert.equal(reads, 4);
assert.deepEqual(result.debugRequests, []);
assert.ok(!JSON.stringify(result).includes("secret"));
assert.equal(h.tabs[0].active, false);
assert.equal(h.tabs[0].url, "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live/webApps/external/");
assert.deepEqual(h.removedTabs, [91]);
});
test("proactive preparation times out without issuing lookup probes", async () => {
let calls = 0;
const h = loadBackground({ messageTimeout: 10000, fetch: async () => { calls += 1; assert.fail("No probes allowed"); } });
const result = await h.dispatch({ type: "opportunitiesExtension.prepareSrSession" });
assert.equal(result.ok, false);
assert.equal(calls, 0);
assert.match(result.error, /session could not be established/);
assert.deepEqual(h.removedTabs, [91]);
});
test("cookie change wakes recovery without waiting for a polling timer and removes its listener", { timeout: 2000 }, async () => {
const fs = require("node:fs");
const vm = require("node:vm");
const path = require("node:path");
const factory = vm.runInNewContext(`${fs.readFileSync(path.join(__dirname, "../src/services/fusion/sr-details.js"), "utf8")}\ncreateFusionSrDetails`);
const listeners = new Set();
let ready = false;
let calls = 0;
let closed = 0;
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
const cookies = [1, 2].map(part => ({ name: `ORA_OCIS_CG_SESSION_idcs-new_${domain}_${part}`, value: `secret-${part}`, domain, storeId: "store" }));
const api = factory({
cookieChanges: { addListener: fn => listeners.add(fn), removeListener: fn => listeners.delete(fn) },
cookiesGetAll: async () => ready ? cookies : [],
tabsCreate: async () => {
setImmediate(() => {
ready = true;
for (const listener of listeners) listener({ cookie: cookies[1] });
});
return { id: 7 };
},
tabsRemove: async () => { closed += 1; },
supportsFirefoxCookieStoreTabs: () => false,
wait: () => new Promise(() => {})
});
const result = await api.requestLookupWithSession(async () => {
calls += 1;
if (!ready) { const error = new Error("Unauthorized"); error.httpStatus = 401; throw error; }
return { ok: true, items: [] };
}, {}, { tab: { cookieStoreId: "store" } });
assert.equal(result.ok, true);
assert.equal(calls, 2);
assert.equal(closed, 1);
assert.equal(listeners.size, 0);
});
test("renewed matching cookie fragments trigger an early retry without leaking session values", async () => {
let reads = 0;
let calls = 0;
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
const cookie = (id, part, value) => ({ name: `ORA_OCIS_CG_SESSION_idcs-${id}_${domain}_${part}`, value, domain, path: "/", session: true });
const h = loadBackground({
cookies: () => {
reads += 1;
if (reads < 3) return [cookie("old", 1, "old-secret-1"), cookie("old", 2, "old-secret-2")];
if (reads === 3) return [cookie("new", 1, "new-secret-1"), cookie("other", 2, "wrong-pair")];
return [cookie("new", 1, "new-secret-1"), cookie("new", 2, "new-secret-2")];
},
fetch: async (_, options) => {
calls += 1;
assert.equal(options.credentials, "include");
assert.equal(options.headers.Cookie, undefined);
if (calls === 1) return response({}, 401);
assert.equal(reads, 4, "retry immediately when both renewed parts match, not on a partial pair");
return response({ items: [] });
}
});
const result = await h.dispatch({ type: "opportunitiesExtension.requestSrLookupLanguage" });
assert.equal(result.ok, true);
assert.equal(calls, 2);
assert.ok(!JSON.stringify(result).includes("secret"));
assert.ok(h.cookieQueries.every(query => query.domain === domain && query.storeId === "test-store"));
assert.deepEqual(h.removedTabs, [91]);
});
for (const family of ["chrome", "firefox"]) {
test(`${family}: simultaneous lookup 401s share an inactive session tab and close it after successful retries`, async () => {
const counts = new Map();
const h = loadBackground({ family, fetch: async url => {
const count = (counts.get(url) || 0) + 1;
counts.set(url, count);
return count < 3 ? response({}, 401) : response({ items: [] });
} });
const results = await Promise.all(["PrimaryPillar", "Language", "TechPlatform", "OpportunityBusiness"].map(name =>
h.dispatch({ type: `opportunitiesExtension.requestSrLookup${name}`, emailAddress: "test@example.invalid" })));
assert.ok(results.every(result => result.ok));
assert.ok(results.every(result => result.debugRequests.length === 3));
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].url, "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live/webApps/external/");
assert.equal(h.tabs[0].cookieStoreId, family === "firefox" ? "test-store" : undefined);
assert.deepEqual(h.removedTabs, [91]);
});
}
test("lookup authentication timeout closes the temporary tab and returns a clear error", async () => {
const h = loadBackground({ messageTimeout: 10000, fetch: async () => response({}, 401) });
const result = await h.dispatch({ type: "opportunitiesExtension.requestSrLookupLanguage" });
assert.equal(result.ok, false);
assert.match(result.error, /session could not be established/);
assert.equal(h.tabs.length, 1);
assert.deepEqual(h.removedTabs, [91]);
});
test("SR language, platform and services use fixed Session endpoints and dynamic email", async () => {
const calls = [];
const h = loadBackground({ fetch: async (url, options) => {
calls.push({ url, options });
return response({ items: url.includes("/services?")
? [{ Id: 123, RecordName: "Consumption", Abstract_c: "Service summary" }]
: [{ code: "CODE", name: "Label", enabledFlag: "Y", displaySequence: 1 }] });
} });
for (const [name, lookup] of [["requestSrLookupLanguage", "SENGG_LANGUAGE"], ["requestSrLookupTechPlatform", "SENGG_TECH_PLATFORM"]]) {
const result = await h.dispatch({ type: `opportunitiesExtension.${name}` });
assert.deepEqual(result.items, [{ code: "CODE", name: "Label" }]);
assert.equal(new URL(calls.at(-1).url).searchParams.get("lookupType"), lookup);
assert.equal(result.debugRequests[0].name, name);
}
const result = await h.dispatch({ type: "opportunitiesExtension.requestSrLookupOpportunityBusiness", emailAddress: "user+test@example.invalid" });
assert.deepEqual(result.items, [{ Id: "123", RecordName: "Consumption", Abstract_c: "Service summary" }]);
const url = new URL(calls.at(-1).url);
assert.equal(url.searchParams.get("emailAddress"), "user+test@example.invalid");
assert.equal(url.searchParams.get("salesObject"), "OPPORTUNITY");
assert.equal(url.searchParams.get("serviceType"), "BUSINESS");
assert.ok(calls.every(call => call.options.headers.Authorization === "Session" && call.options.method === "GET"));
assert.equal((await h.dispatch({ type: "opportunitiesExtension.requestSrLookupOpportunityBusiness" })).ok, false);
assert.equal(calls.length, 3);
});
test("Primary pillar lookup uses Session, orders enabled name/code options and rejects invalid data", async () => {
const calls = [];
const h = loadBackground({ fetch: async (url, options) => {
calls.push({ url, options });
return response({ items: [
{ code: "B", name: "Second", displaySequence: 20, enabledFlag: "Y" },
{ code: "N", name: "Disabled", displaySequence: 1, enabledFlag: "N" },
{ code: "A", name: "First", displaySequence: 10, enabledFlag: "Y" }
] });
} });
const result = await h.dispatch({ type: "opportunitiesExtension.requestSrLookupPrimaryPillar" });
assert.equal(result.ok, true);
assert.deepEqual(result.items, [{ code: "A", name: "First" }, { code: "B", name: "Second" }]);
assert.ok(calls[0].url.includes("SeaaS_initiation/live;profile=PROD"));
assert.ok(calls[0].url.endsWith("lookupType=SENGG_PRIMARY_PILLAR&type=STANDARD"));
assert.equal(calls[0].options.headers.Authorization, "Session");
assert.equal(calls[0].options.method, "GET");
for (const fetch of [async () => response({ items: [{}] }), async () => response({}, 401)]) {
assert.equal((await loadBackground({ fetch, messageTimeout: 10000 }).dispatch({ type: "opportunitiesExtension.requestSrLookupPrimaryPillar" })).ok, false);
}
});
for (const family of ["chrome", "firefox"]) {
test(`${family}: 401 is returned to the caller without opening tabs`, async () => {
let count = 0;

View File

@@ -4,7 +4,7 @@ const fs = require("node:fs");
const vm = require("node:vm");
const path = require("node:path");
const factory = vm.runInNewContext(`${fs.readFileSync(path.join(__dirname, "../src/features/opportunities/internal-sr-session.js"), "utf8")}\ncreateInternalSrSession`);
function setup(query) {
function setup(query, options = {}) {
let elapsed = 0;
const frames = [], removed = [], delays = [], stopped = [];
const body = { append(frame) { frame.initialSrc = frame.src; frames.push(frame); } };
@@ -13,9 +13,28 @@ function setup(query) {
return { contentWindow: { stop() { stopped.push(true); } }, style: {}, attributes: {}, listeners: {}, setAttribute(k, v) { this.attributes[k] = v; },
addEventListener(k, fn) { this.listeners[k] = fn; }, remove() { removed.push(this); } };
} };
const api = factory({ query, environment: { document, performance: { now: () => elapsed }, setTimeout: (fn, ms) => { delays.push(ms); elapsed += ms; return setImmediate(fn); } } });
const api = factory({ ...options, query, environment: { document, performance: { now: () => elapsed }, setTimeout: (fn, ms) => { delays.push(ms); elapsed += ms; return setImmediate(fn); } } });
return { api, frames, removed, delays, stopped };
}
test("lookup requests recover together using the configured page and preserve each result", async () => {
let authenticated = false;
let calls = 0;
const h = setup(async message => {
if (++calls <= 4) return { ok: false, httpStatus: 401 };
authenticated = true;
return { ok: true, items: [message.type] };
}, { sessionUrl: "https://eeho.fa.us2.oraclecloud.com/?view=Show%20Service%20Request", sessionLabel: "Service Request" });
const types = ["pillar", "language", "platform", "services"];
const results = await Promise.all(types.map(type => h.api.request({ type })));
assert.deepEqual(results, types.map(type => [type]));
assert.equal(authenticated, true);
assert.equal(h.frames.length, 1);
assert.match(h.frames[0].initialSrc, /Show%20Service%20Request$/);
assert.equal(h.frames[0].title, "Service Request session initialization");
assert.equal(h.removed.length, 1);
assert.equal(h.stopped.length, 1);
});
test("401 initializes a hidden iframe in the calling document and removes it on success", async () => {
let calls = 0;
const h = setup(async () => ++calls < 3 ? { ok: false, httpStatus: 401 } : { ok: true, items: [{ SrId: 1 }] });
@@ -44,6 +63,33 @@ test("an immediately available session stops the iframe without waiting for load
assert.equal(h.removed.length, 1);
});
test("lookup 401 errors from older background responses still initialize the session", async () => {
for (const failure of [
{ ok: false, error: "requestSrLookupLanguage failed with status 401." },
{ ok: false, debugRequests: [{ httpStatus: 401 }] },
{ ok: false, httpStatus: "401" }
]) {
let calls = 0;
const h = setup(async () => ++calls <= 2 ? failure : { ok: true, items: ["ready"] });
assert.deepEqual(await h.api.request("language"), ["ready"]);
assert.equal(h.frames.length, 1);
assert.equal(h.removed.length, 1);
}
});
test("a lookup that still returns 401 after shared recovery initializes its own session", async () => {
const calls = new Map();
const h = setup(async id => {
const count = (calls.get(id) || 0) + 1;
calls.set(id, count);
return count <= (id === "language" ? 1 : 2)
? { ok: false, httpStatus: 401 } : { ok: true, items: [id] };
});
assert.deepEqual(await Promise.all([h.api.request("language"), h.api.request("platform")]), [["language"], ["platform"]]);
assert.equal(h.frames.length, 2);
assert.equal(h.removed.length, 2);
});
test("cross-origin redirects still allow iframe removal when stop is blocked", async () => {
let calls = 0;
const h = setup(async () => {

View File

@@ -0,0 +1,47 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
test("preparation runs once, shares pending work and reports loading and completion", async () => {
const app = loadApplication("content");
const progress = [];
let requests = 0;
let resolveUser, resolveSession;
app.currentUser.ensure = () => new Promise(resolve => { resolveUser = resolve; });
app.currentUser.allowsCrmActivities = () => true;
app.activityProgress.update = (...args) => progress.push(args);
app.debugContentLog.appendBackgroundRequestLog = () => {};
app.platformContentMessages.sendRuntimeMessage = message => {
assert.equal(message.type, "opportunitiesExtension.prepareSrSession");
requests += 1;
return new Promise(resolve => { resolveSession = resolve; });
};
const first = app.srSessionPreparation.ensure();
assert.equal(app.srSessionPreparation.ensure(), first);
assert.equal(requests, 0, "identity must load first");
resolveUser();
await new Promise(resolve => setImmediate(resolve));
assert.deepEqual(progress, [["session", "running"]]);
resolveSession({ ok: true });
await first;
await app.srSessionPreparation.ensure();
assert.equal(requests, 1);
assert.deepEqual(progress.at(-1), ["session", "complete"]);
});
test("SALES_REP skips preparation; failed preparation does not auto-repeat or reject modal loading", async () => {
for (const allowed of [false, true]) {
const app = loadApplication("content");
let requests = 0;
const progress = [];
app.currentUser.ensure = async () => {};
app.currentUser.allowsCrmActivities = () => allowed;
app.activityProgress.update = (...args) => progress.push(args);
app.debugContentLog.appendBackgroundRequestLog = () => {};
app.platformContentMessages.sendRuntimeMessage = async () => { requests += 1; return { ok: false }; };
await app.srSessionPreparation.ensure();
await app.srSessionPreparation.ensure();
assert.equal(requests, allowed ? 1 : 0);
assert.deepEqual(progress, allowed ? [["session", "running"], ["session", "error"]] : []);
}
});

View File

@@ -57,7 +57,7 @@ function loadContent({ fetch = unexpectedFetch, storage = new Map(), xsrf = null
};
}
function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage = {}, cookies = () => [], storageError = false } = {}) {
function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage = {}, cookies = () => [], storageError = false, messageTimeout = 2000 } = {}) {
const time = clock();
const listeners = [];
const progress = [];
@@ -123,7 +123,7 @@ function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage =
function dispatch(message) {
return new Promise((resolve, reject) => {
let replies = 0;
const timer = setTimeout(() => reject(new Error("Message did not receive a response")), 2000);
const timer = setTimeout(() => reject(new Error("Message did not receive a response")), messageTimeout);
const pending = listeners[0](message, sender, result => {
clearTimeout(timer);
assert.equal(++replies, 1, "Message must receive exactly one response");

View File

@@ -35,6 +35,89 @@ function setup() {
return { app, host, document, nodes, form };
}
test("missing SR plus opens pillar form, handles retry, ignores closed requests and respects roles", async () => {
const h = setup();
h.app.opportunitiesTableState.opportunitiesTableState.items = [
{ OptyNumber: "A", Name: "<Opportunity A>", CustomerAccount: { PartyUniqueName: "Customer A" }, EffectiveDate: "2026-11-30" }
];
h.app.opportunitiesQueryState.periodRequestVersion = 1;
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [{ SrNumber: "SR1", OpportunityName_Id_c: { OptyNumber: "B" } }];
h.app.debugContentView.renderDebugPanel = () => {};
await h.app.opportunityCrmServiceRequests.load("test", 1);
const calls = [];
const extraCalls = [];
h.app.currentUser.getUserId = () => "user@example.invalid";
h.app.platformContentMessages.sendRuntimeMessage = message => {
if (message.type.endsWith("requestSrLookupPrimaryPillar")) return new Promise(resolve => calls.push({ message, resolve }));
extraCalls.push(message);
return Promise.resolve({ ok: true, items: message.type.endsWith("requestSrLookupOpportunityBusiness")
? [{ Id: "123", RecordName: "Service", Abstract_c: "<Description>" }]
: [{ code: "CODE", name: "Label" }] });
};
const badge = h.app.opportunityCrmServiceRequests.createBadge("A");
const plus = badge.children.find(node => node.tag === "button");
assert.ok(plus);
assert.equal(plus.textContent, "+");
assert.ok(!h.app.opportunityCrmServiceRequests.createBadge("B").children.some(node => node.className === "opportunities-extension-add-action"));
plus.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", "<Opportunity A>", "30/11/2026"]);
assert.equal(calls.length, 1);
assert.equal(calls[0].message.type, "opportunitiesExtension.requestSrLookupPrimaryPillar");
const field = () => h.nodes().find(node => node.name === "primaryPillar");
assert.equal(field().disabled, true);
calls[0].resolve({ ok: false, error: "Unauthorized" });
await flush();
assert.equal(h.form().attributes["data-state"], "error");
assert.equal(extraCalls.length, 3);
assert.equal(extraCalls.find(call => call.type.endsWith("requestSrLookupOpportunityBusiness")).emailAddress, "user@example.invalid");
assert.equal(h.nodes().find(node => node.name === "customerDeadline").type, "date");
assert.equal(h.nodes().find(node => node.name === "description").tag, "textarea");
assert.equal(h.nodes().find(node => node.name === "language").children[1].value, "CODE");
assert.equal(h.nodes().find(node => node.name === "platform").children[1].textContent, "Label");
assert.equal(h.nodes().find(node => node.name === "service").value, "123");
assert.equal(h.nodes().find(node => node.name === "service").type, "radio");
assert.ok(h.nodes().some(node => node.textContent === "<Description>"));
h.nodes().find(node => node.textContent === "Retry" && !node.hidden).click();
calls[1].resolve({ ok: true, items: [{ code: "SaaS_HCM", name: "SaaS - HCM" }] });
await flush();
assert.equal(field().disabled, false);
assert.equal(field().children[1].value, "SaaS_HCM");
assert.equal(field().children[1].textContent, "SaaS - HCM");
const search = h.nodes().find(node => node.id === `${field().id}-search`);
assert.equal(field().hidden, true);
assert.equal(search.disabled, false);
search.value = "hcm";
search.listeners.input();
const options = h.nodes().find(node => node.id === search.attributes["aria-controls"]);
assert.equal(options.children.length, 1);
assert.equal(options.children[0].textContent, "SaaS - HCM");
search.listeners.keydown({ key: "ArrowDown", preventDefault() {} });
search.listeners.keydown({ key: "Enter", preventDefault() {} });
assert.equal(field().value, "SaaS_HCM");
assert.equal(search.value, "SaaS - HCM");
assert.equal(options.hidden, true);
search.value = "does not exist";
search.listeners.input();
assert.equal(options.textContent, "No results");
search.listeners.keydown({ key: "Escape", preventDefault() {}, stopPropagation() {} });
assert.equal(search.value, "SaaS - HCM");
assert.ok(h.form(), "Escape closes only the options");
assert.equal(calls.length, 2, "searching does not issue another request");
h.nodes().find(node => node.textContent === "Cancel").click();
assert.equal(h.host.children.length, 0);
h.app.opportunitiesTableState.opportunitiesTableState.items = [];
plus.click();
assert.deepEqual(h.nodes().filter(node => node.tag === "dd").map(node => node.textContent), ["-", "-", "-"]);
h.app.opportunityCrmServiceRequests.reset();
calls[2].resolve({ ok: true, items: [] });
await flush();
assert.equal(h.host.children.length, 0);
h.app.currentUser.allowsCrmActivities = () => false;
plus.click();
assert.equal(h.host.children.length, 0);
});
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");