Implemented the customer installed-base view
This commit is contained in:
159
tests/installBase.test.cjs
Normal file
159
tests/installBase.test.cjs
Normal file
@@ -0,0 +1,159 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const { loadBackground, response, plain } = require("./support/characterization.cjs");
|
||||
const { loadApplication } = require("./support/application.cjs");
|
||||
|
||||
const authorizations = {
|
||||
accountSummary: { name: "Account-Ti-Authorization", value: "synthetic-summary" },
|
||||
installBase: { name: "synthetic-provider-header", value: "synthetic-installed" }
|
||||
};
|
||||
const message = { type: "opportunitiesExtension.requestInstallBase", partyNumber: "customer-123", authorizations };
|
||||
|
||||
for (const family of ["chrome", "firefox"]) {
|
||||
test(`${family}: summary registryId, SAIB header, payload, pagination and protected 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: { label: "Account-Ti-Authorization", value: "synthetic-summary" } },
|
||||
{ tokenService: "OCICONS-TS", uiaasHeader: { label: "G2m-Authorization", value: "synthetic-consumption" } },
|
||||
{ tokenService: "SAIB-TS", uiaasHeader: { label: "synthetic-provider-header", value: "synthetic-installed" } }
|
||||
]);
|
||||
if (url.endsWith("accountsummary")) return response({ id: "NOT-THE-REGISTRY", registryId: "registry/test" });
|
||||
const offset = Number(new URL(url).searchParams.get("offset"));
|
||||
return response({ items: [{ productName: `Product ${offset}` }], offset, limit: 25, hasMore: offset === 0 });
|
||||
} });
|
||||
const tokens = await h.dispatch({ type: "opportunitiesExtension.requestConsumerTokenService" });
|
||||
assert.deepEqual(tokens.authorizations.installBase, { ...authorizations.installBase, expires: "" });
|
||||
const result = await h.dispatch({ ...message, authorizations: tokens.authorizations });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.items.length, 2);
|
||||
assert.deepEqual(JSON.parse(calls[1].options.body), [{ key: "regId", value: "customer-123" }]);
|
||||
assert.equal(calls[1].options.headers[authorizations.accountSummary.name], "synthetic-summary");
|
||||
for (const [index, call] of calls.slice(2).entries()) {
|
||||
assert.ok(call.url.includes(`/account/registry%2Ftest?orderBy=currentSupport%3Adesc&limit=25&offset=${index}`));
|
||||
assert.equal(call.options.method, "POST");
|
||||
assert.equal(call.options.credentials, "include");
|
||||
assert.equal(call.options.headers[authorizations.installBase.name], "synthetic-installed");
|
||||
assert.deepEqual(JSON.parse(call.options.body), {
|
||||
text: "", accountHierarchy: "single", accountScope: "all", productScope: "all",
|
||||
installStatus: ["active", "inactive", "unsupported"], productIds: null,
|
||||
contractEndDate: { to: null, from: null }, contractStartDate: { to: null, from: null },
|
||||
purchaseStartDate: { to: null, from: null }, registryIds: [], gecPartyNumbers: null
|
||||
});
|
||||
}
|
||||
assert.deepEqual(result.debugRequests.map(entry => entry.name), ["requestAccountSummary", "requestInstallBase", "requestInstallBase"]);
|
||||
assert.doesNotMatch(JSON.stringify(result.debugRequests), /synthetic-installed|synthetic-summary/);
|
||||
});
|
||||
}
|
||||
|
||||
test("missing optional SAIB does not break existing consumer token flow", async () => {
|
||||
const h = loadBackground({ fetch: async () => response([
|
||||
{ tokenService: "ACCP-TS", uiaasHeader: { value: "synthetic-summary" } },
|
||||
{ tokenService: "OCICONS-TS", uiaasHeader: { value: "synthetic-consumption" } }
|
||||
]) });
|
||||
const tokens = await h.dispatch({ type: "opportunitiesExtension.requestConsumerTokenService" });
|
||||
assert.equal(tokens.ok, true);
|
||||
const result = await h.dispatch({ ...message, authorizations: tokens.authorizations });
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.error, /SAIB-TS/);
|
||||
assert.equal(result.debugRequests.length, 0);
|
||||
});
|
||||
|
||||
test("invalid registry, pagination, HTTP, JSON and network failures preserve error/debug envelopes", async () => {
|
||||
for (const kind of ["registry", "stalled", "offset", "collection", "http", "json", "network"]) {
|
||||
const h = loadBackground({ fetch: async url => {
|
||||
if (url.endsWith("accountsummary")) return response(kind === "registry" ? { id: "wrong" } : { registryId: "test" });
|
||||
if (kind === "network") throw new Error("Synthetic network failure");
|
||||
if (kind === "http") return response({}, 403);
|
||||
if (kind === "json") return new Response("<html>fallback</html>");
|
||||
if (kind === "collection") return response({ items: "invalid", hasMore: false });
|
||||
return response({ items: [], offset: kind === "offset" ? 25 : 0, hasMore: true });
|
||||
} });
|
||||
const result = await h.dispatch(message);
|
||||
assert.equal(result.ok, false, kind);
|
||||
assert.ok(result.error, kind);
|
||||
assert.equal(result.debugRequests.length, kind === "registry" ? 1 : 2);
|
||||
assert.doesNotMatch(JSON.stringify(result.debugRequests), /synthetic-installed|synthetic-summary/);
|
||||
}
|
||||
});
|
||||
|
||||
class Element {
|
||||
constructor(tag, document) { this.tag = tag; this.document = document; this.children = []; this.dataset = {}; this.attributes = {}; this.listeners = {}; }
|
||||
append(...nodes) { nodes.forEach(node => { node.parent = this; }); this.children.push(...nodes); }
|
||||
set textContent(value) { this.text = value; this.children = []; }
|
||||
get textContent() { return this.text || ""; }
|
||||
setAttribute(key, value) { this.attributes[key] = value; }
|
||||
addEventListener(key, fn) { this.listeners[key] = fn; }
|
||||
focus() { this.document.activeElement = this; }
|
||||
remove() { if (this.parent) this.parent.children = this.parent.children.filter(node => node !== this); this.parent = null; }
|
||||
get isConnected() { return Boolean(this.parent); }
|
||||
click() { return this.listeners.click(); }
|
||||
}
|
||||
function setup() {
|
||||
const document = { createElement: tag => new Element(tag, document), createElementNS: (_, tag) => new Element(tag, document),
|
||||
getElementById: id => id === app.contracts.MODAL_ID ? overlay : null };
|
||||
const overlay = new Element("main", document);
|
||||
const app = loadApplication("content", { document, Intl, Date });
|
||||
return { app, overlay, document };
|
||||
}
|
||||
const customer = { CustomerAccount: { PartyNumber: "customer-123", PartyUniqueName: "Synthetic customer" } };
|
||||
const walk = node => [node, ...node.children.flatMap(walk)];
|
||||
|
||||
test("modal shows skeleton, renders only requested fields safely and ignores closed/replaced responses", () => {
|
||||
const { app, overlay, document } = setup();
|
||||
const first = app.installBaseView.openInstallBaseModal(customer);
|
||||
const modal = overlay.children[0];
|
||||
assert.equal(modal.attributes["aria-busy"], "true");
|
||||
assert.equal(walk(modal).filter(node => node.tag === "th").length, 12);
|
||||
const second = app.installBaseView.openInstallBaseModal(customer);
|
||||
first.render([{ productName: "Stale product" }]);
|
||||
assert.equal(overlay.children[0].attributes["aria-busy"], "true");
|
||||
second.render([{ productName: "<script>unsafe</script>", supportStatus: "ACTIVE", quantity: 0,
|
||||
currentSupport: 1234.56, licenseValue: 0, supportStartDate: "2026-03-12", secret: "not-visible" }]);
|
||||
const current = overlay.children[0];
|
||||
const cells = walk(current).filter(node => node.tag === "td");
|
||||
assert.equal(cells.length, 12);
|
||||
assert.equal(cells[2].textContent, "<script>unsafe</script>");
|
||||
assert.equal(cells[2].children.length, 0);
|
||||
assert.equal(cells[3].textContent, "1,234.56");
|
||||
assert.equal(cells[5].children[0].dataset.status, "ACTIVE");
|
||||
assert.equal(cells[7].textContent, "0");
|
||||
assert.equal(cells[10].textContent, "0.00");
|
||||
assert.ok(cells[8].textContent.includes("2026"));
|
||||
current.listeners.keydown({ key: "Tab", preventDefault() {} });
|
||||
assert.equal(document.activeElement.attributes.role, "region");
|
||||
assert.equal(app.installBaseView.closeInstallBaseModal(), true);
|
||||
second.render([], "Late error");
|
||||
assert.equal(overlay.children.length, 0);
|
||||
assert.equal(app.installBaseView.closeInstallBaseModal(), false);
|
||||
});
|
||||
|
||||
test("action opens before auth, deduplicates in-flight customer queries, and allows retry after failure", async () => {
|
||||
const { app, overlay } = setup();
|
||||
let resolveAuth;
|
||||
let calls = 0;
|
||||
const logs = [];
|
||||
app.authContentSession.requestConsumerTokenServiceOnce = () => new Promise(resolve => { resolveAuth = resolve; });
|
||||
app.platformContentMessages.sendRuntimeMessage = async message => {
|
||||
calls += 1;
|
||||
assert.equal(message.type, "opportunitiesExtension.requestInstallBase");
|
||||
assert.equal(message.partyNumber, "customer-123");
|
||||
assert.deepEqual(plain(message.authorizations), authorizations);
|
||||
return { ok: false, error: "Synthetic failure", debugRequests: ["debug"] };
|
||||
};
|
||||
app.debugContentLog.appendBackgroundRequestLog = entries => logs.push(entries);
|
||||
const button = app.installBaseAction.createInstallBaseButton(customer);
|
||||
const first = button.click();
|
||||
assert.equal(overlay.children[0].attributes["aria-busy"], "true");
|
||||
const second = button.click();
|
||||
resolveAuth(authorizations);
|
||||
await Promise.all([first, second]);
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(logs.length, 1);
|
||||
assert.ok(walk(overlay.children[0]).some(node => node.textContent === "Synthetic failure"));
|
||||
const retry = button.click();
|
||||
resolveAuth(authorizations);
|
||||
await retry;
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
Reference in New Issue
Block a user