Visual improvements to debug mode for better information visibility
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -20,6 +20,35 @@ function fixture() {
|
||||
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", () => {
|
||||
const h = fixture();
|
||||
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", () => {
|
||||
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"]
|
||||
|
||||
@@ -12,14 +12,17 @@ function createDebugDom() {
|
||||
addEventListener(name, listener) { this.listeners[name] = listener; },
|
||||
focus() { document.activeElement = this; },
|
||||
querySelectorAll(selector) {
|
||||
return descendants(this).filter(node => node.tag === "details"
|
||||
&& node.getAttribute("data-request-id") && (!selector.includes("[open]") || node.open));
|
||||
return descendants(this).filter(node => selector.startsWith(".")
|
||||
? (node.className || "").split(" ").includes(selector.slice(1))
|
||||
: node.tag === "details" && node.getAttribute("data-request-id") && (!selector.includes("[open]") || node.open));
|
||||
}
|
||||
};
|
||||
}
|
||||
const panel = element("aside");
|
||||
Object.assign(document, { getElementById: () => panel, createElement: element, createElementNS: (_, tag) => element(tag) });
|
||||
return { document, panel, descendants };
|
||||
const modal = element("main");
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user