Files
opportunities-extension/tests/debugTabs.test.cjs

316 lines
19 KiB
JavaScript

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 { createDebugDom } = require("./support/debug-dom.cjs");
function fixture() {
const dom = createDebugDom();
const app = loadApplication("content", { document: dom.document });
const add = (id, name, second, status = "success") => app.debugContentLog.appendBackgroundRequestLog([{
debugKey: id, name, status, startedAt: `2026-09-18T12:00:${String(second).padStart(2, "0")}.000Z`,
metadata: name === "requestOpptyProducts" ? { optyNumber: "SYNTH-1", offset: 0 } : {},
requestHeaders: { Authorization: "synthetic-private" }
}]);
const nodes = () => dom.descendants(dom.panel);
const tab = category => nodes().find(node => node.getAttribute("data-debug-category") === category);
const ids = () => nodes().filter(node => node.tag === "details").map(node => node.getAttribute("data-request-id"));
const select = category => { const button = tab(category); button.focus(); button.listeners.click(); };
return { ...dom, app, add, nodes, tab, ids, select };
}
test("debug renders opportunities lookup before HTTP headers exist and preserves failed resolution details", () => {
const h = fixture();
const entry = h.app.debugContentLog.logRequestStart({ name: "requestOpportunitiesByContact", method: "GET", url: "https://example.invalid/opportunities",
metadata: { httpRequestSent: false, phase: "Resolving contact PartyNumber" } });
h.app.debugContentLog.logRequestFailure(entry, "Contact PartyNumber unavailable. HTTP request was not sent.");
assert.ok(h.nodes().some(node => node.textContent === "Debug Requests"));
assert.equal(h.ids().length, 1);
assert.equal(h.app.debugContentLog.formatHeaderBlock(undefined), "(none)");
assert.equal(h.app.debugContentLog.formatMetadataBlock(null), "(none)");
assert.match(h.app.debugContentLog.formatMetadataBlock({httpRequestSent:false, offset:0}), /httpRequestSent: false\noffset: 0/);
});
test("catalog and identity secrets are revealed exactly by the eye button and hidden again", () => {
const h = fixture();
const body = JSON.stringify({ result: "catalog-exact-secret", detail: "Preserved" });
h.app.debugContentLog.appendBackgroundRequestLog([{
name: "requestSrTokenCatalog", method: "POST", startedAt: new Date().toISOString(), status: "success",
responseBody: body, requestHeaders: { "vb-proxy-header-x-oracle-ectoken": "header-exact-secret", "x-appbuilder-client-id": "client-exact-secret" }
}, {
name: "requestCurrentUser", method: "GET", startedAt: new Date().toISOString(), status: "success",
responseBody: JSON.stringify({ user: { roles: ["SIMPLE_USER"] }, token: "identity-exact-secret" })
}]);
const text = node => node.text + node.children.map(text).join("");
const toggle = () => h.nodes().find(node => node.getAttribute("data-debug-focus") === "visibility").listeners.click();
assert.doesNotMatch(text(h.panel), /exact-secret/);
toggle();
for (const secret of ["catalog-exact-secret", "header-exact-secret", "client-exact-secret", "identity-exact-secret"]) assert.ok(text(h.panel).includes(secret));
toggle();
assert.doesNotMatch(text(h.panel), /exact-secret/);
assert.equal(h.app.debugState.requestLog[0].responseBody, body, "display masking never overwrites originals");
});
test("request summaries show duration after status, including zero, failure and pending", () => {
const h = fixture();
h.add("duration", "requestOpportunities", 1);
const entry = h.app.debugState.requestLog.at(-1);
for (const [durationMs, status, expected] of [[0, "success", "0 ms"], [325, "success", "325 ms"], [1540, "failed", "1.54 s"], [null, "pending", "In progress"], [null, "failed", "N/A"]]) {
Object.assign(entry, { durationMs, status });
h.app.debugContentView.renderDebugPanel();
const meta = h.nodes().find(node => node.className === "opportunities-extension-debug-summary-meta");
assert.equal(meta.children.at(-1).textContent, expected);
assert.equal(meta.children.at(-2).className, "opportunities-extension-debug-status");
}
});
test("complete large responses stay formatted, masked and colored in both runtimes", () => {
const h = fixture();
const background = loadApplication("background");
const body = JSON.stringify({ items: Array.from({ length: 2000 }, (_, index) => ({
id: index, name: "Synthetic opportunity", access_token: "synthetic-secret",
uiaasHeader: { value: "synthetic-header-secret" }, owner: { name: "Synthetic owner" }
})) });
for (const logger of [h.app.debugContentLog, background.httpBackgroundJson]) {
const preview = logger.createDebugBody(body);
assert.equal(preview.truncated, false);
assert.equal(preview.value, body);
const parsed = JSON.parse(preview.value);
assert.equal(parsed.items.length, 2000);
assert.equal(parsed.items.at(-1).id, 1999);
const formatted = h.app.debugContentLog.formatDebugBody(preview.value);
assert.match(formatted, /\n "items"/);
assert.match(formatted, /"id": 1999/);
assert.doesNotMatch(formatted, /synthetic-secret|synthetic-header-secret/);
const container = h.document.createElement("div");
h.app.debugContentView.appendDebugBlock(container, "Response Body", formatted);
assert.ok(h.descendants(container).some(node => node.className === "opportunities-extension-debug-json-key"));
h.app.debugState.debugSecretsVisible = true;
assert.match(h.app.debugContentLog.formatDebugBody(preview.value), /synthetic-secret/);
h.app.debugState.debugSecretsVisible = false;
assert.equal(logger.createDebugBody('{"ok":true}').value, '{"ok":true}');
const html = "<html>" + "x".repeat(150000) + "</html>";
assert.equal(logger.createDebugBody(html).value, html);
assert.equal(logger.createDebugBody("").value, "(empty response body)");
}
});
test("request categories cover every current family and unknown requests stay visible", () => {
const h = fixture();
for (const [name, category] of Object.entries({
requestTokenRelay: "authentication", requestTokenRelaySalesCommon: "authentication",
requestConsumerTokenService: "authentication", requestCustomerTokenService: "authentication",
requestOpportunities: "opportunities", requestOpportunitiesSavedSearch: "opportunities", requestOpportunitiesByContact: "opportunities", requestOpptyProducts: "products", requestGenerateAiSummary: "ai",
requestQueryAccountContact: "contacts", requestPersistContact: "contacts",
requestAccountSummary: "consumption", requestAccountPlans: "consumption",
requestInstallBase: "installBase", requestCustomerServiceRequest: "serviceRequests", requestSRInfo: "serviceRequests",
requestConsumptionPlanDetail: "consumption",
requestRatecardByPlan: "consumption",
requestConsumptionServiceQuarterly: "consumption", requestConsumptionServiceMonthly: "consumption",
requestConsumptionServiceWeekly: "consumption", requestConsumptionServiceDaily: "consumption", futureRequest: "other"
})) assert.equal(h.app.debugContentView.getRequestCategory({ name }), category, name);
h.app.debugContentView.renderDebugPanel();
assert.ok(h.tab("all"));
assert.equal(h.tab("products"), undefined);
h.add("future", "futureRequest", 0);
h.select("other");
assert.deepEqual(h.ids(), ["1"]);
});
test("All and category views sort by start time, preserve original ids and leave the log untouched", () => {
const h = fixture();
h.add("p-late", "requestOpptyProducts", 30, "pending");
h.add("auth", "requestTokenRelay", 10);
h.add("p-early", "requestOpptyProducts", 20);
assert.deepEqual(h.ids(), ["2", "3", "1"]);
assert.deepEqual(Array.from(h.app.debugState.requestLog, entry => entry.id), [1, 2, 3]);
h.select("products");
assert.deepEqual(h.ids(), ["3", "1"]);
assert.equal(h.tab("products").textContent, "Products (2) / 1 running");
assert.equal(h.nodes().filter(node => node.tag === "time").length, 2);
h.select("all");
assert.deepEqual(h.ids(), ["2", "3", "1"]);
assert.equal(h.tab("all").textContent, "All (3) / 1 running");
});
test("category, expansion, focus and hidden credentials survive asynchronous updates", () => {
const h = fixture();
h.add("p", "requestOpptyProducts", 10, "pending");
h.add("auth", "requestTokenRelay", 5);
h.select("products");
h.nodes().find(node => node.tag === "details").open = true;
h.select("authentication");
h.add("p", "requestOpptyProducts", 10);
assert.deepEqual(h.ids(), ["2"]);
assert.equal(h.tab("authentication").getAttribute("aria-selected"), "true");
assert.equal(h.document.activeElement, h.tab("authentication"));
h.select("products");
assert.equal(h.nodes().find(node => node.tag === "details").open, true);
assert.equal(h.tab("products").textContent, "Products (1)");
assert.ok(!h.nodes().some(node => node.text.includes("synthetic-private")));
h.nodes().find(node => node.tag === "details").open = false;
h.app.debugContentView.renderDebugPanel();
assert.equal(h.nodes().find(node => node.tag === "details").open, false);
});
test("tabs support arrows, Home and End with a single selected tab and labelled panel", () => {
const h = fixture();
h.add("auth", "requestTokenRelay", 1);
h.add("p", "requestOpptyProducts", 2);
h.select("all");
for (const [key, expected] of [["ArrowRight", "authentication"], ["End", "products"], ["ArrowRight", "all"], ["ArrowLeft", "products"], ["Home", "all"]]) {
let prevented = false;
h.document.activeElement.listeners.keydown({ key, preventDefault() { prevented = true; } });
assert.equal(prevented, true);
const selected = h.nodes().filter(node => node.getAttribute("aria-selected") === "true");
assert.equal(selected.length, 1);
assert.equal(selected[0], h.tab(expected));
assert.equal(h.document.activeElement, selected[0]);
assert.equal(h.nodes().find(node => node.getAttribute("role") === "tabpanel").getAttribute("aria-labelledby"), selected[0].id);
}
});
test("dark debug provides explicit readable text for inactive tabs and disclosure arrows", () => {
const css = fs.readFileSync(path.join(__dirname, "../src/features/debug/styles.css"), "utf8");
const rules = [...css.matchAll(/([^{}]+)\{([^{}]+)\}/g)];
const prefix = '.opportunities-extension-modal[data-theme="dark"] ';
const rule = suffix => rules.find(([, selector]) => selector.trim() === prefix + suffix)?.[2];
assert.match(rule(".opportunities-extension-debug-panel"), /color: #f6f4f2/);
assert.match(rule(".opportunities-extension-debug-panel"), /color-scheme: dark/);
assert.match(rule(".opportunities-extension-debug-tabs button"), /color: #f6f4f2/);
assert.match(rule(".opportunities-extension-debug-heading::before"), /color: #c9c5c1/);
assert.match(rule('.opportunities-extension-debug-tabs button[aria-selected="true"]'), /color: #bde7ee/);
assert.match(rule(".opportunities-extension-debug-tabs button:hover"), /background: #383633/);
});
test("full-page debug opens with close focus, closes via Escape/button/shortcut and retains request state", () => {
const h = fixture();
const trigger = h.document.createElement("button");
trigger.isConnected = true;
trigger.focus();
h.panel.hidden = true;
h.panel.inert = true;
h.app.debugContentView.toggleDebugPanel();
assert.equal(h.panel.hidden, false);
assert.equal(h.panel.inert, false);
assert.ok(h.modal.children.includes(h.panel), "debug escapes nested modal containers");
assert.equal(h.panel.getAttribute("role"), "dialog");
assert.equal(h.document.activeElement.getAttribute("aria-label"), "Close debug");
h.add("p", "requestOpptyProducts", 1, "pending");
h.nodes().find(node => node.tag === "details").open = true;
const list = h.nodes().find(node => node.getAttribute("role") === "tabpanel");
list.scrollTop = 320;
h.add("p", "requestOpptyProducts", 1, "success");
assert.equal(h.nodes().find(node => node.getAttribute("role") === "tabpanel").scrollTop, 320);
assert.equal(h.document.activeElement.getAttribute("aria-label"), "Close debug");
let stopped = false;
h.panel.onkeydown({ key: "Escape", preventDefault() {}, stopPropagation() { stopped = true; } });
assert.equal(stopped, true);
assert.equal(h.panel.hidden, true);
assert.equal(h.modal.hidden, false, "Escape never closes the main modal");
assert.equal(h.document.activeElement, trigger);
h.app.debugContentView.toggleDebugPanel();
assert.equal(h.nodes().find(node => node.tag === "details").open, true);
const eye = h.nodes().find(node => node.getAttribute("data-debug-focus") === "visibility");
const last = h.nodes().filter(node => node.tag === "summary").at(-1);
eye.focus();
h.panel.onkeydown({ key: "Tab", shiftKey: true, preventDefault() {} });
assert.equal(h.document.activeElement, last);
h.panel.onkeydown({ key: "Tab", preventDefault() {} });
assert.equal(h.document.activeElement, eye);
h.nodes().find(node => node.getAttribute("data-debug-focus") === "close").listeners.click();
assert.equal(h.panel.hidden, true);
h.app.debugContentView.toggleDebugPanel();
h.app.debugContentView.toggleDebugPanel();
assert.equal(h.panel.hidden, true);
});
test("colored JSON preserves exact masked text, safe strings, eye toggle and HTTP families", () => {
const h = fixture();
const response = JSON.stringify({ access_token: "synthetic-private", name: '<img src=x onerror="bad()">', count: 12.5, enabled: false, missing: null });
h.app.debugContentLog.appendBackgroundRequestLog([{
name: "requestTokenRelay", method: "POST", startedAt: new Date().toISOString(),
httpStatus: 401, status: "failed", responseBody: response, requestHeaders: { Authorization: "synthetic-private" }
}]);
const text = node => node.text + node.children.map(text).join("");
const responseBlock = () => h.nodes().find(node => node.tag === "section" && node.children[0]?.textContent === "Response Body").children[1];
assert.equal(text(responseBlock()), h.app.debugContentLog.formatDebugBody(response));
assert.doesNotMatch(text(h.panel), /synthetic-private/);
for (const kind of ["key", "string", "number", "literal"]) {
assert.ok(h.nodes().some(node => node.className === `opportunities-extension-debug-json-${kind}`));
}
assert.equal(h.nodes().filter(node => node.tag === "img").length, 0);
assert.equal(h.nodes().find(node => node.className === "opportunities-extension-debug-http-status").getAttribute("data-http-class"), "4");
assert.equal(h.nodes().find(node => node.className === "opportunities-extension-debug-method").textContent, "POST");
const eye = h.nodes().find(node => node.getAttribute("data-debug-focus") === "visibility");
eye.focus(); eye.listeners.click();
assert.equal(text(responseBlock()), JSON.stringify(JSON.parse(response), null, 2));
assert.equal(h.document.activeElement.getAttribute("data-debug-focus"), "visibility");
h.document.activeElement.listeners.click();
assert.doesNotMatch(text(h.panel), /synthetic-private/);
const container = h.document.createElement("div");
h.app.debugContentView.appendDebugBlock(container, "Response Body", "<html>Not JSON</html>");
assert.equal(container.children[0].children[1].textContent, "<html>Not JSON</html>");
assert.equal(container.children[0].children[1].children.length, 0);
});
test("compact, protected and string-encoded JSON are formatted and colored with secrets both hidden and visible", () => {
const h = fixture();
const value = { items: [{ id: 1, name: "Synthetic", active: true }], access_token: "synthetic-private" };
const compact = JSON.stringify(value);
const text = node => node.text + node.children.map(text).join("");
for (const body of [compact, `\uFEFF${compact}`, `)]}',\n${compact}`, `while(1);${compact}`, JSON.stringify(compact)]) {
for (const visible of [false, true]) {
h.app.debugState.debugSecretsVisible = visible;
const formatted = h.app.debugContentLog.formatDebugBody(body);
assert.equal(formatted, JSON.stringify(visible ? value : h.app.debugContentLog.maskSensitiveDebugValue(value), null, 2));
const container = h.document.createElement("div");
h.app.debugContentView.appendDebugBlock(container, "Response Body", formatted);
assert.equal(text(container.children[0].children[1]), formatted);
assert.ok(h.descendants(container).some(node => node.className === "opportunities-extension-debug-json-key"));
if (!visible) assert.doesNotMatch(text(container), /synthetic-private/);
}
}
for (const text of ["<html>Login</html>", '{"items":[\n... truncated 100 characters', '"ordinary string"']) {
assert.equal(h.app.debugContentLog.formatDebugBody(text), text);
}
const dense = JSON.stringify(Array.from({ length: 3100 }, () => 1));
const container = h.document.createElement("div");
const formatted = h.app.debugContentLog.formatDebugBody(dense);
h.app.debugContentView.appendDebugBlock(container, "Response Body", formatted);
assert.equal(text(container.children[0].children[1]), formatted);
assert.equal(h.descendants(container).filter(node => node.className === "opportunities-extension-debug-json-number").length, 3100);
});
test("full-page debug CSS defines responsive bounds and readable semantic colors in both themes", () => {
const css = fs.readFileSync(path.join(__dirname, "../src/features/debug/styles.css"), "utf8");
const rule = css.match(/\.opportunities-extension-modal \.opportunities-extension-debug-panel \{([^}]+)\}/)[1];
assert.match(rule, /inset: 0/);
assert.match(rule, /max-height: none/);
assert.match(rule, /grid-template-rows: auto auto minmax\(0, 1fr\)/);
assert.match(css, /grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/);
assert.match(css, /grid-template-columns: minmax\(0, 1fr\)/);
const dark = css.match(/\.opportunities-extension-modal\[data-theme="dark"\] \.opportunities-extension-debug-panel \{([^}]+)\}/)[1];
const luminance = hex => hex.slice(1).match(/../g).map(value => parseInt(value, 16) / 255)
.map(value => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4)
.reduce((sum, value, index) => sum + value * [.2126, .7152, .0722][index], 0);
for (const theme of [rule, dark]) {
const vars = Object.fromEntries([...theme.matchAll(/--debug-([\w-]+): (#[a-f0-9]{6})/g)].map(match => [match[1], match[2]]));
for (const [foreground, background] of [["success", "success-bg"], ["failed", "failed-bg"], ["pending", "pending-bg"], ["info", "info-bg"],
...["text", "key", "string", "number", "literal"].map(key => [key, "code"])]) {
const values = [luminance(vars[foreground]), luminance(vars[background])].sort((a, b) => b - a);
assert.ok((values[0] + .05) / (values[1] + .05) >= 4.5, `${foreground} on ${background}`);
}
}
});
test("scoped debug styles are packaged after the unchanged main stylesheet", () => {
const root = path.join(__dirname, "..");
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css", "src/features/opportunities/customer-table-search.css", "src/features/opportunities/calendar.css"]
.concat("src/features/manage-time/styles.css", "src/features/products/ramp-projection.css", "src/features/org-chart/styles.css", "src/content/styles/tables.css")
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(css)};`));
assert.ok(!composeScript("background").includes("opportunities-extension-debug-tabs"));
});