171 lines
8.8 KiB
JavaScript
171 lines
8.8 KiB
JavaScript
const assert = require("node:assert/strict");
|
|
const { test } = require("node:test");
|
|
const { loadApplication } = require("./support/application.cjs");
|
|
const flush = () => new Promise(resolve => setImmediate(resolve));
|
|
|
|
test("internal SR uses Session without TokenRelay and reports unavailable iframe host", async () => {
|
|
const h = setup();
|
|
let renewals = 0;
|
|
const sent = [];
|
|
h.app.authSessionState.accessToken = "stale-token";
|
|
h.app.authTokenRelay.requestTokenRelay = async () => `fresh-${++renewals}`;
|
|
h.app.debugContentView.renderDebugPanel = () => {};
|
|
h.app.platformContentMessages.sendRuntimeMessage = async message => {
|
|
sent.push(message);
|
|
return { ok: false, httpStatus: 401, error: "Unauthorized" };
|
|
};
|
|
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [{ SrId: "123", SrNumber: "SR123", OpportunityName_Id_c: { OptyNumber: "OP1" } }];
|
|
await h.app.opportunityCrmServiceRequests.load("synthetic", 1);
|
|
const badge = h.app.opportunityCrmServiceRequests.createBadge("OP1");
|
|
h.modal.append(badge);
|
|
badge.listeners.pointerenter();
|
|
await flush();
|
|
assert.equal(renewals, 0);
|
|
assert.equal(sent.length, 1);
|
|
assert.equal(sent[0].token, undefined);
|
|
assert.ok(sent.every(item => item.type === "opportunitiesExtension.requestInternalServiceRequest"));
|
|
assert.ok(h.walk(h.panel()).some(node => node.attributes.role === "alert"));
|
|
});
|
|
|
|
function setup() {
|
|
const document = { activeElement: null, listeners: new Map() };
|
|
const window = { innerWidth: 400, innerHeight: 600, listeners: new Map(), setTimeout, clearTimeout };
|
|
for (const target of [document, window]) {
|
|
target.addEventListener = (key, fn) => target.listeners.set(key, fn);
|
|
target.removeEventListener = key => target.listeners.delete(key);
|
|
}
|
|
const walk = node => [node, ...node.children.flatMap(walk)];
|
|
function element(tag) {
|
|
return {
|
|
tag, children: [], attributes: {}, listeners: {}, style: {}, textContent: "",
|
|
append(...nodes) { nodes.forEach(node => { node.parent = this; this.children.push(node); }); },
|
|
remove() { this.parent.children = this.parent.children.filter(node => node !== this); this.parent = null; },
|
|
setAttribute(key, value) { this.attributes[key] = value; },
|
|
addEventListener(key, fn) { this.listeners[key] = fn; },
|
|
contains(target) { return walk(this).includes(target); },
|
|
get isConnected() { return this === modal || !!this.parent?.isConnected; },
|
|
getBoundingClientRect() { return { left: 350, bottom: 550, height: 250 }; },
|
|
querySelector() { return walk(this).find(node => node.tag === "a" || node.tag === "button"); },
|
|
focus() { document.activeElement = this; this.listeners.focus?.(); }
|
|
};
|
|
}
|
|
const modal = element("main");
|
|
Object.assign(document, {
|
|
createElement: element, createElementNS: (_, tag) => element(tag),
|
|
getElementById: id => id === "opportunities-extension-modal" ? modal : null
|
|
});
|
|
const copied = [];
|
|
const app = loadApplication("content", { document, window, setTimeout, clearTimeout,
|
|
navigator: { clipboard: { writeText: async value => copied.push(value) } } });
|
|
app.currentUser.allowsCrmActivities = () => true;
|
|
// Keep the copy test independent of the toast's own DOM/timer implementation.
|
|
modal.querySelector = () => ({ classList: { toggle() {} } });
|
|
window.setTimeout = () => 0;
|
|
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [
|
|
{ SrId: "NOT-THE-URL-ID", SrNumber: "SR /?&1", Title: "<script>synthetic</script>",
|
|
ReportedHours_c: 0, OpportunityName_Id_c: { OptyNumber: "OP1" } },
|
|
{ SrNumber: "SR2", OpportunityName_Id_c: { OptyNumber: "OP1" } }
|
|
];
|
|
app.opportunitiesQueryState.periodRequestVersion = 1;
|
|
const panel = () => modal.children.find(node => node.className === "opportunities-extension-sr-popover");
|
|
return { app, document, window, modal, walk, copied, panel };
|
|
}
|
|
|
|
test("SR popover uses encoded SrNumber, safe details and existing copy control for every record", async () => {
|
|
const { app, modal, walk, copied, panel } = setup();
|
|
await app.opportunityCrmServiceRequests.load("synthetic", 1);
|
|
const badge = app.opportunityCrmServiceRequests.createBadge("OP1");
|
|
modal.append(badge);
|
|
badge.listeners.pointerenter();
|
|
const nodes = walk(panel());
|
|
const links = nodes.filter(node => node.tag === "a");
|
|
assert.equal(links.length, 2);
|
|
for (const link of links) {
|
|
assert.equal(link.parent.children[0].textContent, "SR Number:");
|
|
assert.equal(link.parent.children[2].className, "opportunities-extension-copy-button");
|
|
}
|
|
const details = nodes.filter(node => node.className === "opportunities-extension-sr-details");
|
|
assert.equal(details.length, 2);
|
|
details.forEach(node => assert.doesNotMatch(node.textContent, /SR Number:|SR \/\?&1|SR2/));
|
|
assert.equal(links[0].href, "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=SR%20%2F%3F%261");
|
|
assert.equal(links[0].rel, "noopener noreferrer");
|
|
assert.equal(links[0].target, "_blank");
|
|
assert.ok(nodes.some(node => node.textContent.includes("<script>synthetic</script>")));
|
|
assert.equal(nodes.some(node => node.tag === "script"), false);
|
|
const copies = nodes.filter(node => node.className === "opportunities-extension-copy-button");
|
|
assert.equal(copies.length, 2);
|
|
assert.equal(copies[0].title, "Copy SR number");
|
|
copies[0].listeners.click({ preventDefault() {}, stopPropagation() {} });
|
|
await Promise.resolve();
|
|
assert.deepEqual(copied, ["SR /?&1"]);
|
|
assert.equal(panel().style.width, "376px");
|
|
app.opportunityCrmServiceRequests.reset();
|
|
assert.equal(panel(), undefined);
|
|
});
|
|
|
|
test("SR popover pins on click, closes on Escape/outside/scroll/reset and removes global listeners", async () => {
|
|
const { app, modal, document, window, panel } = setup();
|
|
await app.opportunityCrmServiceRequests.load("synthetic", 1);
|
|
const badge = app.opportunityCrmServiceRequests.createBadge("OP1");
|
|
modal.append(badge);
|
|
badge.listeners.click({ stopPropagation() {} });
|
|
assert.equal(badge.attributes["aria-expanded"], "true");
|
|
assert.ok(panel().contains(document.activeElement));
|
|
document.listeners.get("keydown")({ key: "Escape", preventDefault() {}, stopPropagation() {} });
|
|
assert.equal(panel(), undefined);
|
|
assert.equal(document.activeElement, badge);
|
|
assert.equal(document.listeners.size, 0);
|
|
assert.equal(window.listeners.size, 0);
|
|
badge.listeners.pointerenter();
|
|
document.listeners.get("pointerdown")({ target: modal });
|
|
assert.equal(panel(), undefined);
|
|
badge.listeners.pointerenter();
|
|
document.listeners.get("scroll")({ target: panel() });
|
|
assert.ok(panel(), "scroll inside the tooltip keeps it open");
|
|
document.listeners.get("scroll")({ target: modal });
|
|
assert.equal(panel(), undefined);
|
|
badge.listeners.pointerenter();
|
|
app.opportunityCrmServiceRequests.reset();
|
|
assert.equal(panel(), undefined);
|
|
assert.equal(document.listeners.size, 0);
|
|
});
|
|
|
|
test("SR tooltip loads nested details with copy/link and empty text, discarding stale responses", async () => {
|
|
const h = setup();
|
|
const calls = [];
|
|
h.app.authSessionState.accessToken = "crm-token";
|
|
h.app.authTokenRelay.requestTokenRelay = async () => "fresh-crm-token";
|
|
h.app.debugContentView.renderDebugPanel = () => {};
|
|
h.app.platformContentMessages.sendRuntimeMessage = message => new Promise(resolve => calls.push({ message, resolve }));
|
|
await h.app.opportunityCrmServiceRequests.load("synthetic", 1);
|
|
const badge = h.app.opportunityCrmServiceRequests.createBadge("OP1");
|
|
h.modal.append(badge);
|
|
badge.listeners.pointerenter();
|
|
await flush();
|
|
assert.equal(calls.length, 2);
|
|
assert.equal(calls[0].message.token, undefined);
|
|
assert.equal(calls[0].message.srId, "NOT-THE-URL-ID");
|
|
assert.ok(h.walk(h.panel()).some(node => node.textContent === "Loading SR details..."));
|
|
calls[0].resolve({ ok: true, items: [{ SrId: 99, SrNumber: "SR &child", Title: "<child>", ServiceNew_c: "Queue" }] });
|
|
calls[1].resolve({ ok: true, items: [] });
|
|
await flush();
|
|
const nodes = h.walk(h.panel());
|
|
const link = nodes.find(node => node.tag === "a" && node.textContent === "SR &child");
|
|
assert.equal(link.parent.children[0].textContent, "Internal SR Number:");
|
|
assert.ok(link.href.endsWith("srNumber=SR%20%26child&srId=99"));
|
|
assert.equal(link.parent.children[2].title, "Copy SR number");
|
|
assert.ok(nodes.some(node => node.textContent.includes("Service Queue: Queue")));
|
|
assert.ok(nodes.some(node => node.textContent === "No further details."));
|
|
h.app.crmSrPopover?.close();
|
|
h.app.opportunityCrmServiceRequests.reset();
|
|
badge.listeners.pointerenter();
|
|
const oldPanel = h.panel();
|
|
await flush();
|
|
h.app.opportunityCrmServiceRequests.reset();
|
|
calls[2].resolve({ ok: true, items: [{ SrNumber: "STALE" }] });
|
|
calls[3].resolve({ ok: false, error: "Forbidden" });
|
|
await flush();
|
|
assert.equal(h.panel(), undefined);
|
|
assert.ok(!h.walk(oldPanel).some(node => node.textContent === "STALE"));
|
|
});
|