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("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/D"]]) { 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("large response previews stay valid, 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, limit] of [[h.app.debugContentLog, 12000], [background.httpBackgroundJson, 100000]]) { const preview = logger.createDebugBody(body); assert.equal(preview.truncated, true); assert.ok(preview.value.length <= limit); const parsed = JSON.parse(preview.value); assert.match(parsed.debugNotice, /Partial response/); assert.ok(parsed.preview.items.length > 0); assert.ok(parsed.preview.items.length < 2000); const formatted = h.app.debugContentLog.formatDebugBody(preview.value); assert.match(formatted, /\n "debugNotice"/); 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}'); assert.match(logger.createDebugBody("" + "x".repeat(limit + 1000)).value, /truncated/); } }); 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", requestOpptyProducts: "products", requestGenerateAiSummary: "ai", requestAccountSummary: "consumption", requestAccountPlans: "consumption", requestInstallBase: "installBase", requestCustomerServiceRequest: "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.app.debugContentView.toggleDebugPanel(); assert.equal(h.panel.hidden, false); 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: '', 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", "Not JSON"); assert.equal(container.children[0].children[1].textContent, "Not JSON"); 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 ["Login", '{"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"] .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")); });