Visual improvements to debug mode for better information visibility
This commit is contained in:
14
README.md
14
README.md
@@ -126,6 +126,20 @@ duration or error. Cache hits do not create HTTP entries. Product diagnostics
|
|||||||
mask authorization and sensitive response headers before messaging the source
|
mask authorization and sensitive response headers before messaging the source
|
||||||
tab/frame; this change adds no timeout, retry or authentication behavior.
|
tab/frame; this change adds no timeout, retry or authentication behavior.
|
||||||
|
|
||||||
|
Debug Requests opens as a full-page view inside the extension using Ctrl+D, without
|
||||||
|
opening a browser tab or entering native browser fullscreen. Close, Escape or Ctrl+D
|
||||||
|
returns to the underlying view. Header/category navigation remains above the
|
||||||
|
scrollable timeline; expanded details use two columns on wide screens and one on
|
||||||
|
narrow screens. Theme-specific colors distinguish success, failure, running,
|
||||||
|
HTTP methods/statuses, request/response sections and JSON tokens. Highlighting
|
||||||
|
uses safe text nodes only after the existing sensitive-value masking. Compact JSON,
|
||||||
|
supported protection prefixes and JSON documents wrapped in strings are normalized
|
||||||
|
and indented, including while sensitive values are visible. Large JSON responses
|
||||||
|
are captured as bounded, valid JSON previews with an explicit omission notice,
|
||||||
|
preserving indentation, highlighting and sensitive-field masking. Already incomplete
|
||||||
|
or invalid JSON remains plain text. The visibility toggle retains its existing
|
||||||
|
behavior. Focus, expansion and timeline scroll position survive log updates.
|
||||||
|
|
||||||
Debug tabs group Authentication, Opportunities, Products, AI, Consumption, Installed Base and Service Requests
|
Debug tabs group Authentication, Opportunities, Products, AI, Consumption, Installed Base and Service Requests
|
||||||
requests; unknown request types appear under Other. Only categories with entries
|
requests; unknown request types appear under Other. Only categories with entries
|
||||||
are shown. All preserves the complete timeline. Every tab sorts by request start
|
are shown. All preserves the complete timeline. Every tab sorts by request start
|
||||||
|
|||||||
56
dist/chromium/background.js
vendored
56
dist/chromium/background.js
vendored
@@ -19,6 +19,48 @@ function createMessageTypes() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source: src/shared/debug-body.js
|
||||||
|
function createDebugBodyPreview(text, limit) {
|
||||||
|
if (text.length <= limit) return { value: text || "(empty response body)", truncated: false };
|
||||||
|
try {
|
||||||
|
const normalized = text.replace(/^\uFEFF/, "").trimStart()
|
||||||
|
.replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, "");
|
||||||
|
let value = JSON.parse(normalized);
|
||||||
|
for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) {
|
||||||
|
try { value = JSON.parse(value); } catch { break; }
|
||||||
|
}
|
||||||
|
// Bound retained data without cutting JSON syntax or losing sensitive field names.
|
||||||
|
let remaining = Math.max(0, limit - 256);
|
||||||
|
const visit = (node, depth = 0) => {
|
||||||
|
if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]";
|
||||||
|
if (node === null || typeof node !== "object") {
|
||||||
|
const serialized = JSON.stringify(node);
|
||||||
|
if (serialized.length > remaining) {
|
||||||
|
remaining = 0;
|
||||||
|
return "[omitted from debug preview]";
|
||||||
|
}
|
||||||
|
remaining -= serialized.length + 1;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const result = Array.isArray(node) ? [] : Object.create(null);
|
||||||
|
remaining -= 2;
|
||||||
|
for (const key of Object.keys(node)) {
|
||||||
|
const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2;
|
||||||
|
if (remaining < cost + 64) break;
|
||||||
|
remaining -= cost;
|
||||||
|
result[key] = visit(node[key], depth + 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }),
|
||||||
|
truncated: true
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Source: src/background/contracts.js
|
// Source: src/background/contracts.js
|
||||||
function createBackgroundContracts() {
|
function createBackgroundContracts() {
|
||||||
const ORACLE_DOMAIN = "eeho.fa.us2.oraclecloud.com";
|
const ORACLE_DOMAIN = "eeho.fa.us2.oraclecloud.com";
|
||||||
@@ -742,6 +784,7 @@ function createSpaResponseShapes() {
|
|||||||
|
|
||||||
// Source: src/services/http/background-json.js
|
// Source: src/services/http/background-json.js
|
||||||
function createHttpBackgroundJson({
|
function createHttpBackgroundJson({
|
||||||
|
previewDebugBody,
|
||||||
DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT,
|
||||||
environment
|
environment
|
||||||
}) {
|
}) {
|
||||||
@@ -863,17 +906,7 @@ function createHttpBackgroundJson({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createDebugBody(text) {
|
function createDebugBody(text) {
|
||||||
if (text.length > DEBUG_BODY_LIMIT) {
|
return previewDebugBody(text, DEBUG_BODY_LIMIT);
|
||||||
return {
|
|
||||||
value: `${text.slice(0, DEBUG_BODY_LIMIT)}\n... truncated ${text.length - DEBUG_BODY_LIMIT} characters`,
|
|
||||||
truncated: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: text || "(empty response body)",
|
|
||||||
truncated: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDebugHeaders(headers) {
|
function normalizeDebugHeaders(headers) {
|
||||||
@@ -1831,6 +1864,7 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
});
|
});
|
||||||
const spaResponseShapes = createSpaResponseShapes();
|
const spaResponseShapes = createSpaResponseShapes();
|
||||||
const httpBackgroundJson = createHttpBackgroundJson({
|
const httpBackgroundJson = createHttpBackgroundJson({
|
||||||
|
previewDebugBody: createDebugBodyPreview,
|
||||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
|
|||||||
174
dist/chromium/content.js
vendored
174
dist/chromium/content.js
vendored
File diff suppressed because one or more lines are too long
56
dist/firefox/background.js
vendored
56
dist/firefox/background.js
vendored
@@ -19,6 +19,48 @@ function createMessageTypes() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source: src/shared/debug-body.js
|
||||||
|
function createDebugBodyPreview(text, limit) {
|
||||||
|
if (text.length <= limit) return { value: text || "(empty response body)", truncated: false };
|
||||||
|
try {
|
||||||
|
const normalized = text.replace(/^\uFEFF/, "").trimStart()
|
||||||
|
.replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, "");
|
||||||
|
let value = JSON.parse(normalized);
|
||||||
|
for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) {
|
||||||
|
try { value = JSON.parse(value); } catch { break; }
|
||||||
|
}
|
||||||
|
// Bound retained data without cutting JSON syntax or losing sensitive field names.
|
||||||
|
let remaining = Math.max(0, limit - 256);
|
||||||
|
const visit = (node, depth = 0) => {
|
||||||
|
if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]";
|
||||||
|
if (node === null || typeof node !== "object") {
|
||||||
|
const serialized = JSON.stringify(node);
|
||||||
|
if (serialized.length > remaining) {
|
||||||
|
remaining = 0;
|
||||||
|
return "[omitted from debug preview]";
|
||||||
|
}
|
||||||
|
remaining -= serialized.length + 1;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const result = Array.isArray(node) ? [] : Object.create(null);
|
||||||
|
remaining -= 2;
|
||||||
|
for (const key of Object.keys(node)) {
|
||||||
|
const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2;
|
||||||
|
if (remaining < cost + 64) break;
|
||||||
|
remaining -= cost;
|
||||||
|
result[key] = visit(node[key], depth + 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }),
|
||||||
|
truncated: true
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Source: src/background/contracts.js
|
// Source: src/background/contracts.js
|
||||||
function createBackgroundContracts() {
|
function createBackgroundContracts() {
|
||||||
const ORACLE_DOMAIN = "eeho.fa.us2.oraclecloud.com";
|
const ORACLE_DOMAIN = "eeho.fa.us2.oraclecloud.com";
|
||||||
@@ -742,6 +784,7 @@ function createSpaResponseShapes() {
|
|||||||
|
|
||||||
// Source: src/services/http/background-json.js
|
// Source: src/services/http/background-json.js
|
||||||
function createHttpBackgroundJson({
|
function createHttpBackgroundJson({
|
||||||
|
previewDebugBody,
|
||||||
DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT,
|
||||||
environment
|
environment
|
||||||
}) {
|
}) {
|
||||||
@@ -863,17 +906,7 @@ function createHttpBackgroundJson({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createDebugBody(text) {
|
function createDebugBody(text) {
|
||||||
if (text.length > DEBUG_BODY_LIMIT) {
|
return previewDebugBody(text, DEBUG_BODY_LIMIT);
|
||||||
return {
|
|
||||||
value: `${text.slice(0, DEBUG_BODY_LIMIT)}\n... truncated ${text.length - DEBUG_BODY_LIMIT} characters`,
|
|
||||||
truncated: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: text || "(empty response body)",
|
|
||||||
truncated: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDebugHeaders(headers) {
|
function normalizeDebugHeaders(headers) {
|
||||||
@@ -1831,6 +1864,7 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
});
|
});
|
||||||
const spaResponseShapes = createSpaResponseShapes();
|
const spaResponseShapes = createSpaResponseShapes();
|
||||||
const httpBackgroundJson = createHttpBackgroundJson({
|
const httpBackgroundJson = createHttpBackgroundJson({
|
||||||
|
previewDebugBody: createDebugBodyPreview,
|
||||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
|
|||||||
174
dist/firefox/content.js
vendored
174
dist/firefox/content.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"content": [
|
"content": [
|
||||||
"src/shared/messages.js",
|
"src/shared/messages.js",
|
||||||
|
"src/shared/debug-body.js",
|
||||||
"src/content/contracts.js",
|
"src/content/contracts.js",
|
||||||
"src/auth/session-state.js",
|
"src/auth/session-state.js",
|
||||||
"src/auth/sales-common-state.js",
|
"src/auth/sales-common-state.js",
|
||||||
@@ -62,6 +63,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
"background": [
|
"background": [
|
||||||
"src/shared/messages.js",
|
"src/shared/messages.js",
|
||||||
|
"src/shared/debug-body.js",
|
||||||
"src/background/contracts.js",
|
"src/background/contracts.js",
|
||||||
"src/features/products/cache-state.js",
|
"src/features/products/cache-state.js",
|
||||||
"src/auth/consumer-session.js",
|
"src/auth/consumer-session.js",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
});
|
});
|
||||||
const spaResponseShapes = createSpaResponseShapes();
|
const spaResponseShapes = createSpaResponseShapes();
|
||||||
const httpBackgroundJson = createHttpBackgroundJson({
|
const httpBackgroundJson = createHttpBackgroundJson({
|
||||||
|
previewDebugBody: createDebugBodyPreview,
|
||||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -447,11 +447,13 @@ function createContentApplication(overrides = {}) {
|
|||||||
|
|
||||||
});
|
});
|
||||||
const debugContentLog = createDebugContentLog({
|
const debugContentLog = createDebugContentLog({
|
||||||
|
previewDebugBody: createDebugBodyPreview,
|
||||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||||
debugState,
|
debugState,
|
||||||
renderDebugPanel: (...args) => debugContentView.renderDebugPanel(...args)
|
renderDebugPanel: (...args) => debugContentView.renderDebugPanel(...args)
|
||||||
});
|
});
|
||||||
const debugContentView = createDebugContentView({
|
const debugContentView = createDebugContentView({
|
||||||
|
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
|
||||||
DEBUG_PANEL_ID: contracts.DEBUG_PANEL_ID,
|
DEBUG_PANEL_ID: contracts.DEBUG_PANEL_ID,
|
||||||
MODAL_ID: contracts.MODAL_ID,
|
MODAL_ID: contracts.MODAL_ID,
|
||||||
debugState,
|
debugState,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
function createDebugContentLog({
|
function createDebugContentLog({
|
||||||
|
previewDebugBody,
|
||||||
DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT,
|
||||||
debugState,
|
debugState,
|
||||||
renderDebugPanel
|
renderDebugPanel
|
||||||
@@ -98,17 +99,7 @@ function createDebugContentLog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createDebugBody(text) {
|
function createDebugBody(text) {
|
||||||
if (text.length > DEBUG_BODY_LIMIT) {
|
return previewDebugBody(text, DEBUG_BODY_LIMIT);
|
||||||
return {
|
|
||||||
value: `${text.slice(0, DEBUG_BODY_LIMIT)}\n... truncated ${text.length - DEBUG_BODY_LIMIT} characters`,
|
|
||||||
truncated: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: text || "(empty response body)",
|
|
||||||
truncated: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function headersToObject(headers) {
|
function headersToObject(headers) {
|
||||||
@@ -137,13 +128,16 @@ function createDebugContentLog({
|
|||||||
|
|
||||||
function formatDebugBody(body) {
|
function formatDebugBody(body) {
|
||||||
const text = body || "(none)";
|
const text = body || "(none)";
|
||||||
|
|
||||||
if (debugState.debugSecretsVisible) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(maskSensitiveDebugValue(JSON.parse(text)), null, 2);
|
const normalized = String(text).replace(/^\uFEFF/, "").trimStart()
|
||||||
|
.replace(/^\)\]\}',?\s*/, "")
|
||||||
|
.replace(/^while\s*\(1\);\s*/, "");
|
||||||
|
let value = JSON.parse(normalized);
|
||||||
|
// Some services serialize their JSON document inside a JSON string.
|
||||||
|
for (let depth = 0; depth < 2 && typeof value === "string" && /^[\s]*[\[{]/.test(value); depth += 1) {
|
||||||
|
try { value = JSON.parse(value); } catch { break; }
|
||||||
|
}
|
||||||
|
return JSON.stringify(debugState.debugSecretsVisible ? value : maskSensitiveDebugValue(value), null, 2);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ function createDebugContentView({
|
|||||||
formatDebugBody,
|
formatDebugBody,
|
||||||
formatHeaderBlock,
|
formatHeaderBlock,
|
||||||
formatMetadataBlock,
|
formatMetadataBlock,
|
||||||
|
createCloseIcon,
|
||||||
environment
|
environment
|
||||||
}) {
|
}) {
|
||||||
const { document } = environment;
|
const { document } = environment;
|
||||||
@@ -16,6 +17,7 @@ function createDebugContentView({
|
|||||||
];
|
];
|
||||||
let selectedCategory = "all";
|
let selectedCategory = "all";
|
||||||
const expandedIds = new Set();
|
const expandedIds = new Set();
|
||||||
|
let returnFocus = null;
|
||||||
|
|
||||||
function getRequestCategory(entry) {
|
function getRequestCategory(entry) {
|
||||||
if (/^request(?:TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication";
|
if (/^request(?:TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication";
|
||||||
@@ -45,8 +47,22 @@ function createDebugContentView({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
panel.hidden = !panel.hidden;
|
if (!panel.hidden) {
|
||||||
|
closeDebugPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
returnFocus = document.activeElement;
|
||||||
|
panel.hidden = false;
|
||||||
renderDebugPanel();
|
renderDebugPanel();
|
||||||
|
panel.querySelectorAll(".opportunities-extension-debug-close-button")[0]?.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDebugPanel() {
|
||||||
|
const panel = document.getElementById(DEBUG_PANEL_ID);
|
||||||
|
if (!panel) return;
|
||||||
|
panel.hidden = true;
|
||||||
|
if (returnFocus?.isConnected) returnFocus.focus({ preventScroll: true });
|
||||||
|
returnFocus = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDebugPanel() {
|
function renderDebugPanel() {
|
||||||
@@ -62,14 +78,21 @@ function createDebugContentView({
|
|||||||
else expandedIds.delete(id);
|
else expandedIds.delete(id);
|
||||||
}
|
}
|
||||||
const focusedCategory = document.activeElement && document.activeElement.getAttribute("data-debug-category");
|
const focusedCategory = document.activeElement && document.activeElement.getAttribute("data-debug-category");
|
||||||
|
const focusedControl = document.activeElement && document.activeElement.getAttribute("data-debug-focus");
|
||||||
|
const listScrollTop = panel.querySelectorAll(".opportunities-extension-debug-list")[0]?.scrollTop || 0;
|
||||||
const scrollTop = panel.scrollTop;
|
const scrollTop = panel.scrollTop;
|
||||||
panel.textContent = "";
|
panel.textContent = "";
|
||||||
|
panel.setAttribute("role", "dialog");
|
||||||
|
panel.setAttribute("aria-modal", "true");
|
||||||
|
panel.setAttribute("aria-labelledby", `${DEBUG_PANEL_ID}-title`);
|
||||||
|
const focusTargets = new Map();
|
||||||
|
|
||||||
const panelHeader = document.createElement("header");
|
const panelHeader = document.createElement("header");
|
||||||
panelHeader.className = "opportunities-extension-debug-panel-header";
|
panelHeader.className = "opportunities-extension-debug-panel-header";
|
||||||
|
|
||||||
const heading = document.createElement("div");
|
const heading = document.createElement("div");
|
||||||
const title = document.createElement("h2");
|
const title = document.createElement("h2");
|
||||||
|
title.id = `${DEBUG_PANEL_ID}-title`;
|
||||||
title.textContent = "Debug Requests";
|
title.textContent = "Debug Requests";
|
||||||
|
|
||||||
const summary = document.createElement("p");
|
const summary = document.createElement("p");
|
||||||
@@ -81,6 +104,8 @@ function createDebugContentView({
|
|||||||
const visibilityButton = document.createElement("button");
|
const visibilityButton = document.createElement("button");
|
||||||
visibilityButton.type = "button";
|
visibilityButton.type = "button";
|
||||||
visibilityButton.className = "opportunities-extension-icon-button opportunities-extension-debug-visibility-button";
|
visibilityButton.className = "opportunities-extension-icon-button opportunities-extension-debug-visibility-button";
|
||||||
|
visibilityButton.setAttribute("data-debug-focus", "visibility");
|
||||||
|
focusTargets.set("visibility", visibilityButton);
|
||||||
visibilityButton.setAttribute("aria-pressed", String(debugState.debugSecretsVisible));
|
visibilityButton.setAttribute("aria-pressed", String(debugState.debugSecretsVisible));
|
||||||
visibilityButton.setAttribute("aria-label", debugState.debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values");
|
visibilityButton.setAttribute("aria-label", debugState.debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values");
|
||||||
visibilityButton.title = debugState.debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values";
|
visibilityButton.title = debugState.debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values";
|
||||||
@@ -89,7 +114,19 @@ function createDebugContentView({
|
|||||||
debugState.debugSecretsVisible = !debugState.debugSecretsVisible;
|
debugState.debugSecretsVisible = !debugState.debugSecretsVisible;
|
||||||
renderDebugPanel();
|
renderDebugPanel();
|
||||||
});
|
});
|
||||||
panelHeader.append(heading, visibilityButton);
|
const closeButton = document.createElement("button");
|
||||||
|
closeButton.type = "button";
|
||||||
|
closeButton.className = "opportunities-extension-icon-button opportunities-extension-debug-close-button";
|
||||||
|
closeButton.title = "Close debug";
|
||||||
|
closeButton.setAttribute("aria-label", "Close debug");
|
||||||
|
closeButton.setAttribute("data-debug-focus", "close");
|
||||||
|
closeButton.append(createCloseIcon());
|
||||||
|
closeButton.addEventListener("click", closeDebugPanel);
|
||||||
|
focusTargets.set("close", closeButton);
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
actions.className = "opportunities-extension-debug-actions";
|
||||||
|
actions.append(visibilityButton, closeButton);
|
||||||
|
panelHeader.append(heading, actions);
|
||||||
|
|
||||||
const entries = getChronologicalRequests();
|
const entries = getChronologicalRequests();
|
||||||
const availableCategories = categories.filter(([key]) => key === "all" || entries.some(entry => getRequestCategory(entry) === key));
|
const availableCategories = categories.filter(([key]) => key === "all" || entries.some(entry => getRequestCategory(entry) === key));
|
||||||
@@ -136,6 +173,9 @@ function createDebugContentView({
|
|||||||
list.setAttribute("role", "tabpanel");
|
list.setAttribute("role", "tabpanel");
|
||||||
list.setAttribute("aria-labelledby", `${DEBUG_PANEL_ID}-tab-${selectedCategory}`);
|
list.setAttribute("aria-labelledby", `${DEBUG_PANEL_ID}-tab-${selectedCategory}`);
|
||||||
list.tabIndex = 0;
|
list.tabIndex = 0;
|
||||||
|
list.setAttribute("data-debug-focus", "list");
|
||||||
|
focusTargets.set("list", list);
|
||||||
|
const summaries = [];
|
||||||
|
|
||||||
if (debugState.requestLog.length === 0) {
|
if (debugState.requestLog.length === 0) {
|
||||||
const empty = document.createElement("div");
|
const empty = document.createElement("div");
|
||||||
@@ -152,6 +192,9 @@ function createDebugContentView({
|
|||||||
|
|
||||||
const heading = document.createElement("summary");
|
const heading = document.createElement("summary");
|
||||||
heading.className = "opportunities-extension-debug-heading";
|
heading.className = "opportunities-extension-debug-heading";
|
||||||
|
heading.setAttribute("data-debug-focus", `request-${entry.id}`);
|
||||||
|
focusTargets.set(`request-${entry.id}`, heading);
|
||||||
|
summaries.push(heading);
|
||||||
|
|
||||||
const name = document.createElement("strong");
|
const name = document.createElement("strong");
|
||||||
name.textContent = `${entry.id}. ${entry.name}`;
|
name.textContent = `${entry.id}. ${entry.name}`;
|
||||||
@@ -162,6 +205,7 @@ function createDebugContentView({
|
|||||||
const httpStatus = document.createElement("span");
|
const httpStatus = document.createElement("span");
|
||||||
httpStatus.className = "opportunities-extension-debug-http-status";
|
httpStatus.className = "opportunities-extension-debug-http-status";
|
||||||
httpStatus.textContent = entry.httpStatus ? String(entry.httpStatus) : "pending";
|
httpStatus.textContent = entry.httpStatus ? String(entry.httpStatus) : "pending";
|
||||||
|
httpStatus.setAttribute("data-http-class", entry.httpStatus ? String(Math.floor(entry.httpStatus / 100)) : "pending");
|
||||||
|
|
||||||
const status = document.createElement("span");
|
const status = document.createElement("span");
|
||||||
status.className = "opportunities-extension-debug-status";
|
status.className = "opportunities-extension-debug-status";
|
||||||
@@ -173,7 +217,11 @@ function createDebugContentView({
|
|||||||
startedTime.className = "opportunities-extension-debug-time";
|
startedTime.className = "opportunities-extension-debug-time";
|
||||||
startedTime.textContent = formatDebugTime(entry.startedAt);
|
startedTime.textContent = formatDebugTime(entry.startedAt);
|
||||||
startedTime.title = `Started: ${entry.startedAt.toLocaleString()}`;
|
startedTime.title = `Started: ${entry.startedAt.toLocaleString()}`;
|
||||||
summaryMeta.append(startedTime, httpStatus, status);
|
const method = document.createElement("span");
|
||||||
|
method.className = "opportunities-extension-debug-method";
|
||||||
|
method.textContent = entry.method || "GET";
|
||||||
|
method.setAttribute("data-method", method.textContent.toUpperCase());
|
||||||
|
summaryMeta.append(startedTime, method, httpStatus, status);
|
||||||
|
|
||||||
heading.append(name, summaryMeta);
|
heading.append(name, summaryMeta);
|
||||||
|
|
||||||
@@ -200,7 +248,23 @@ function createDebugContentView({
|
|||||||
});
|
});
|
||||||
|
|
||||||
panel.append(panelHeader, tabs, list);
|
panel.append(panelHeader, tabs, list);
|
||||||
|
panel.onkeydown = event => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
closeDebugPanel();
|
||||||
|
} else if (event.key === "Tab") {
|
||||||
|
const controls = [visibilityButton, closeButton, ...tabButtons.filter(button => button.tabIndex === 0), list, ...summaries];
|
||||||
|
const index = controls.indexOf(document.activeElement);
|
||||||
|
if (index < 0 || (event.shiftKey && index === 0) || (!event.shiftKey && index === controls.length - 1)) {
|
||||||
|
event.preventDefault();
|
||||||
|
controls[event.shiftKey ? controls.length - 1 : 0].focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
if (focusedCategory) tabButtons.find(button => button.getAttribute("data-debug-category") === selectedCategory).focus({ preventScroll: true });
|
if (focusedCategory) tabButtons.find(button => button.getAttribute("data-debug-category") === selectedCategory).focus({ preventScroll: true });
|
||||||
|
else if (focusedControl) focusTargets.get(focusedControl)?.focus({ preventScroll: true });
|
||||||
|
list.scrollTop = listScrollTop;
|
||||||
panel.scrollTop = scrollTop;
|
panel.scrollTop = scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,15 +304,41 @@ function createDebugContentView({
|
|||||||
|
|
||||||
function appendDebugBlock(container, label, value) {
|
function appendDebugBlock(container, label, value) {
|
||||||
const block = document.createElement("section");
|
const block = document.createElement("section");
|
||||||
|
block.className = "opportunities-extension-debug-block";
|
||||||
|
block.setAttribute("data-direction", label.startsWith("Response") ? "response" : "request");
|
||||||
const heading = document.createElement("h3");
|
const heading = document.createElement("h3");
|
||||||
const content = document.createElement("pre");
|
const content = document.createElement("pre");
|
||||||
|
|
||||||
heading.textContent = label;
|
heading.textContent = label;
|
||||||
content.textContent = value;
|
appendJsonTokens(content, value);
|
||||||
block.append(heading, content);
|
block.append(heading, content);
|
||||||
container.append(block);
|
container.append(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendJsonTokens(content, value) {
|
||||||
|
const text = String(value ?? "");
|
||||||
|
try { JSON.parse(text); } catch { content.textContent = text; return; }
|
||||||
|
// Highlight only already-masked display text; never interpret response content as HTML.
|
||||||
|
const tokens = [...text.matchAll(/"(?:[^"\\]|\\.)*"\s*:|"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\b(?:true|false|null)\b/g)];
|
||||||
|
let offset = 0;
|
||||||
|
const append = (textValue, kind) => {
|
||||||
|
if (!textValue) return;
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.textContent = textValue;
|
||||||
|
if (kind) span.className = `opportunities-extension-debug-json-${kind}`;
|
||||||
|
content.append(span);
|
||||||
|
};
|
||||||
|
tokens.forEach(token => {
|
||||||
|
append(text.slice(offset, token.index));
|
||||||
|
const value = token[0];
|
||||||
|
const kind = value.startsWith('"') ? value.endsWith(":") ? "key" : "string"
|
||||||
|
: /^(true|false|null)$/.test(value) ? "literal" : "number";
|
||||||
|
append(value, kind);
|
||||||
|
offset = token.index + value.length;
|
||||||
|
});
|
||||||
|
append(text.slice(offset));
|
||||||
|
}
|
||||||
|
|
||||||
function formatDebugTime(date) {
|
function formatDebugTime(date) {
|
||||||
return date.toLocaleTimeString(undefined, {
|
return date.toLocaleTimeString(undefined, {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
@@ -257,5 +347,5 @@ function createDebugContentView({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { toggleDebugPanel, renderDebugPanel, createDebugVisibilityIcon, appendDebugBlock, formatDebugTime, getRequestCategory, getChronologicalRequests };
|
return { toggleDebugPanel, closeDebugPanel, renderDebugPanel, createDebugVisibilityIcon, appendDebugBlock, formatDebugTime, getRequestCategory, getChronologicalRequests };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,130 @@
|
|||||||
/* Debug navigation only; preserve the extension's existing light/dark theme. */
|
/* Full-page diagnostics, scoped to the extension and its current theme. */
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel {
|
||||||
|
--debug-surface: #fff;
|
||||||
|
--debug-code: #f5f6f7;
|
||||||
|
--debug-text: #312d2a;
|
||||||
|
--debug-muted: #5f5a55;
|
||||||
|
--debug-border: #dedbd7;
|
||||||
|
--debug-success: #22613b;
|
||||||
|
--debug-success-bg: #e1f1e5;
|
||||||
|
--debug-failed: #a12721;
|
||||||
|
--debug-failed-bg: #fce8e6;
|
||||||
|
--debug-pending: #805000;
|
||||||
|
--debug-pending-bg: #fff1ce;
|
||||||
|
--debug-info: #075e87;
|
||||||
|
--debug-info-bg: #e5f1fa;
|
||||||
|
--debug-key: #075e87;
|
||||||
|
--debug-string: #22613b;
|
||||||
|
--debug-number: #865000;
|
||||||
|
--debug-literal: #73429b;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--debug-surface);
|
||||||
|
color: var(--debug-text);
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel {
|
||||||
|
--debug-surface: #292827;
|
||||||
|
--debug-code: #222120;
|
||||||
|
--debug-text: #f6f4f2;
|
||||||
|
--debug-muted: #c9c5c1;
|
||||||
|
--debug-border: #514c47;
|
||||||
|
--debug-success: #a9e3bd;
|
||||||
|
--debug-success-bg: #214d34;
|
||||||
|
--debug-failed: #ffb4ab;
|
||||||
|
--debug-failed-bg: #582c2a;
|
||||||
|
--debug-pending: #ffe099;
|
||||||
|
--debug-pending-bg: #59441e;
|
||||||
|
--debug-info: #a8d9ff;
|
||||||
|
--debug-info-bg: #233f52;
|
||||||
|
--debug-key: #8ed6f0;
|
||||||
|
--debug-string: #a9e3bd;
|
||||||
|
--debug-number: #ffe099;
|
||||||
|
--debug-literal: #d6b7f1;
|
||||||
|
color: #f6f4f2;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
.opportunities-extension-debug-actions { display: flex; gap: 8px; flex: 0 0 auto; }
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-list {
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
align-content: start;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
padding: 2px 8px 12px 2px;
|
||||||
|
}
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item {
|
||||||
|
--debug-state: var(--debug-info);
|
||||||
|
--debug-state-bg: var(--debug-info-bg);
|
||||||
|
border-color: var(--debug-border);
|
||||||
|
border-left-color: var(--debug-state);
|
||||||
|
background: var(--debug-surface);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-success { --debug-state: var(--debug-success); --debug-state-bg: var(--debug-success-bg); }
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-failed { --debug-state: var(--debug-failed); --debug-state-bg: var(--debug-failed-bg); }
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-pending { --debug-state: var(--debug-pending); --debug-state-bg: var(--debug-pending-bg); }
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-status {
|
||||||
|
color: var(--debug-state);
|
||||||
|
background: var(--debug-state-bg);
|
||||||
|
}
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status,
|
||||||
|
.opportunities-extension-debug-method {
|
||||||
|
color: var(--debug-info);
|
||||||
|
background: var(--debug-info-bg);
|
||||||
|
}
|
||||||
|
.opportunities-extension-debug-method {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font: 700 11px/1.2 Consolas, monospace;
|
||||||
|
}
|
||||||
|
.opportunities-extension-debug-method[data-method="POST"] { color: var(--debug-literal); background: var(--debug-code); }
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class="2"] { color: var(--debug-success); background: var(--debug-success-bg); }
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class="4"],
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class="5"] { color: var(--debug-failed); background: var(--debug-failed-bg); }
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class="pending"] { color: var(--debug-pending); background: var(--debug-pending-bg); }
|
||||||
|
.opportunities-extension-debug-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; padding: 0 14px 16px; }
|
||||||
|
.opportunities-extension-debug-block { min-width: 0; border-left: 2px solid var(--debug-info); padding-left: 10px; }
|
||||||
|
.opportunities-extension-debug-block[data-direction="response"] { border-left-color: var(--debug-success); }
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-block h3 { margin: 0 0 8px; color: var(--debug-info); font-size: 12px; }
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-block[data-direction="response"] h3 { color: var(--debug-success); }
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item pre {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 380px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: var(--debug-text);
|
||||||
|
background: var(--debug-code);
|
||||||
|
line-height: 1.6;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
.opportunities-extension-debug-json-key { color: var(--debug-key); }
|
||||||
|
.opportunities-extension-debug-json-string { color: var(--debug-string); }
|
||||||
|
.opportunities-extension-debug-json-number { color: var(--debug-number); }
|
||||||
|
.opportunities-extension-debug-json-literal { color: var(--debug-literal); }
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.opportunities-extension-modal .opportunities-extension-debug-panel { padding: 12px; }
|
||||||
|
.opportunities-extension-debug-details { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
}
|
||||||
.opportunities-extension-debug-tabs {
|
.opportunities-extension-debug-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -62,6 +188,14 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-tabs button {
|
||||||
|
color: #f6f4f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading::before {
|
||||||
|
color: #c9c5c1;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-tabs {
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-tabs {
|
||||||
border-bottom-color: #514c47;
|
border-bottom-color: #514c47;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
function createHttpBackgroundJson({
|
function createHttpBackgroundJson({
|
||||||
|
previewDebugBody,
|
||||||
DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT,
|
||||||
environment
|
environment
|
||||||
}) {
|
}) {
|
||||||
@@ -120,17 +121,7 @@ function createHttpBackgroundJson({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createDebugBody(text) {
|
function createDebugBody(text) {
|
||||||
if (text.length > DEBUG_BODY_LIMIT) {
|
return previewDebugBody(text, DEBUG_BODY_LIMIT);
|
||||||
return {
|
|
||||||
value: `${text.slice(0, DEBUG_BODY_LIMIT)}\n... truncated ${text.length - DEBUG_BODY_LIMIT} characters`,
|
|
||||||
truncated: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: text || "(empty response body)",
|
|
||||||
truncated: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDebugHeaders(headers) {
|
function normalizeDebugHeaders(headers) {
|
||||||
|
|||||||
40
src/shared/debug-body.js
Normal file
40
src/shared/debug-body.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
function createDebugBodyPreview(text, limit) {
|
||||||
|
if (text.length <= limit) return { value: text || "(empty response body)", truncated: false };
|
||||||
|
try {
|
||||||
|
const normalized = text.replace(/^\uFEFF/, "").trimStart()
|
||||||
|
.replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, "");
|
||||||
|
let value = JSON.parse(normalized);
|
||||||
|
for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) {
|
||||||
|
try { value = JSON.parse(value); } catch { break; }
|
||||||
|
}
|
||||||
|
// Bound retained data without cutting JSON syntax or losing sensitive field names.
|
||||||
|
let remaining = Math.max(0, limit - 256);
|
||||||
|
const visit = (node, depth = 0) => {
|
||||||
|
if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]";
|
||||||
|
if (node === null || typeof node !== "object") {
|
||||||
|
const serialized = JSON.stringify(node);
|
||||||
|
if (serialized.length > remaining) {
|
||||||
|
remaining = 0;
|
||||||
|
return "[omitted from debug preview]";
|
||||||
|
}
|
||||||
|
remaining -= serialized.length + 1;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const result = Array.isArray(node) ? [] : Object.create(null);
|
||||||
|
remaining -= 2;
|
||||||
|
for (const key of Object.keys(node)) {
|
||||||
|
const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2;
|
||||||
|
if (remaining < cost + 64) break;
|
||||||
|
remaining -= cost;
|
||||||
|
result[key] = visit(node[key], depth + 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }),
|
||||||
|
truncated: true
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ test("every source belongs to an explicit runtime composition, with no duplicate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.deepEqual(sources.content.filter(file => sources.background.includes(file)), ["src/shared/messages.js"]);
|
assert.deepEqual(sources.content.filter(file => sources.background.includes(file)), ["src/shared/messages.js", "src/shared/debug-body.js"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Redwood cascade is byte-identical to the characterized original", () => {
|
test("Redwood cascade is byte-identical to the characterized original", () => {
|
||||||
|
|||||||
@@ -20,6 +20,35 @@ function fixture() {
|
|||||||
return { ...dom, app, add, nodes, tab, ids, select };
|
return { ...dom, app, add, nodes, tab, ids, select };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("<html>" + "x".repeat(limit + 1000)).value, /truncated/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("request categories cover every current family and unknown requests stay visible", () => {
|
test("request categories cover every current family and unknown requests stay visible", () => {
|
||||||
const h = fixture();
|
const h = fixture();
|
||||||
for (const [name, category] of Object.entries({
|
for (const [name, category] of Object.entries({
|
||||||
@@ -94,6 +123,137 @@ test("tabs support arrows, Home and End with a single selected tab and labelled
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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: '<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", () => {
|
test("scoped debug styles are packaged after the unchanged main stylesheet", () => {
|
||||||
const root = path.join(__dirname, "..");
|
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"]
|
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"]
|
||||||
|
|||||||
@@ -12,14 +12,17 @@ function createDebugDom() {
|
|||||||
addEventListener(name, listener) { this.listeners[name] = listener; },
|
addEventListener(name, listener) { this.listeners[name] = listener; },
|
||||||
focus() { document.activeElement = this; },
|
focus() { document.activeElement = this; },
|
||||||
querySelectorAll(selector) {
|
querySelectorAll(selector) {
|
||||||
return descendants(this).filter(node => node.tag === "details"
|
return descendants(this).filter(node => selector.startsWith(".")
|
||||||
&& node.getAttribute("data-request-id") && (!selector.includes("[open]") || node.open));
|
? (node.className || "").split(" ").includes(selector.slice(1))
|
||||||
|
: node.tag === "details" && node.getAttribute("data-request-id") && (!selector.includes("[open]") || node.open));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const panel = element("aside");
|
const panel = element("aside");
|
||||||
Object.assign(document, { getElementById: () => panel, createElement: element, createElementNS: (_, tag) => element(tag) });
|
const modal = element("main");
|
||||||
return { document, panel, descendants };
|
modal.append(panel);
|
||||||
|
Object.assign(document, { getElementById: id => id === "opportunities-extension-modal" ? modal : panel, createElement: element, createElementNS: (_, tag) => element(tag) });
|
||||||
|
return { document, panel, modal, descendants };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createDebugDom };
|
module.exports = { createDebugDom };
|
||||||
|
|||||||
Reference in New Issue
Block a user