Visual enhancements for dark mode, along with search functionality for service requests and installed base

This commit is contained in:
2026-09-23 10:56:51 -03:00
parent b4d8d9dbc1
commit 6c5db94aaa
17 changed files with 436 additions and 83 deletions

View File

@@ -0,0 +1,34 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { test } = require("node:test");
function luminance(hex) {
const rgb = hex.slice(1).match(/../g).map(value => parseInt(value, 16) / 255)
.map(value => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4);
return rgb[0] * .2126 + rgb[1] * .7152 + rgb[2] * .0722;
}
function contrast(a, b) {
const values = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (values[0] + .05) / (values[1] + .05);
}
for (const feature of ["install-base", "service-requests"]) {
test(`${feature}: explicit dark text colors cover cells, headings, counts and errors with readable contrast`, () => {
const css = fs.readFileSync(path.join(__dirname, `../src/features/${feature}/styles.css`), "utf8");
const rules = [...css.matchAll(/([^{}]+)\{([^{}]+)\}/g)];
const prefix = `.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-${feature}`;
function color(selector) {
const matches = rules.filter(([, selectors, body]) => selectors.split(",").map(value => value.trim()).includes(selector)
&& /(?:^|;)\s*color\s*:/.test(body));
assert.ok(matches.length, `Missing explicit color for ${selector}`);
return matches.at(-1)[2].match(/(?:^|;)\s*color\s*:\s*(#[a-f0-9]{6})/i)[1];
}
for (const suffix of ["-content", "-table", "-table th", "-table td", "-message", '-message[role="alert"]']) {
const foreground = color(prefix + suffix);
const background = suffix.endsWith(" th") ? "#353331" : "#292827";
assert.ok(contrast(foreground, background) >= 4.5, `${suffix} needs at least 4.5:1 contrast`);
}
assert.match(css, /color: #17693a; background: #e1f1e5/, "light status colors remain unchanged");
assert.match(css, /color: #b8e4c6; background: #214d34/, "dark status colors remain unchanged");
});
}

View File

@@ -96,7 +96,7 @@ test("tabs support arrows, Home and End with a single selected tab and labelled
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"]
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"]
.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"));

View File

@@ -100,6 +100,42 @@ function setup() {
const customer = { CustomerAccount: { PartyNumber: "customer-123", PartyUniqueName: "Synthetic customer" } };
const walk = node => [node, ...node.children.flatMap(walk)];
test("installed base searches every displayed column and formatted values, including text entered during loading", () => {
const { app, overlay, document } = setup();
const view = app.installBaseView.openInstallBaseModal(customer);
const modal = overlay.children[0];
const input = walk(modal).find(node => node.tag === "input");
const row = { partyName: "S\u00e3o Customer", productGroupName: "Group Alpha", productName: "Database Beta",
currentSupport: 1234.56, orderNumber: "ORD-321", supportStatus: "ACTIVE", quantityType: "Processor",
quantity: 7, supportStartDate: "2026-03-12", supportEndDate: "2027-04-13", licenseValue: 9876.54, shipDate: "2020-05-14",
hiddenField: "secretword" };
input.value = "sao beta";
input.listeners.input();
assert.equal(modal.attributes["aria-busy"], "true");
view.render([row, { productName: "Other product" }]);
const rows = () => walk(modal).filter(node => node.tag === "td").length / 12;
assert.equal(rows(), 1);
for (const query of [...Object.values(row).slice(0, 12), "1,234.56", "9,876.54", "12/03/2026", "13/04/2027", "14/05/2020"]) {
input.value = String(query); input.listeners.input();
assert.equal(rows(), 1, String(query));
assert.equal(walk(modal).find(node => node.tag === "input"), input, "input is not replaced while typing");
}
input.value = "secretword"; input.listeners.input();
assert.equal(rows(), 0);
assert.ok(walk(modal).some(node => node.textContent.includes("No matching records.")));
input.value = " "; input.listeners.input();
assert.equal(rows(), 2);
input.focus();
modal.listeners.keydown({ key: "Tab", shiftKey: true, preventDefault() {} });
assert.equal(document.activeElement.tag, "button");
view.render([], "Synthetic failure");
input.value = "other"; input.listeners.input();
assert.ok(walk(modal).some(node => node.textContent === "Synthetic failure"));
app.installBaseView.closeInstallBaseModal();
app.installBaseView.openInstallBaseModal(customer).render([row]);
assert.equal(walk(overlay).filter(node => node.tag === "td").length, 12, "reopening starts without an invisible filter");
});
test("modal shows skeleton, renders only requested fields safely and ignores closed/replaced responses", () => {
const { app, overlay, document } = setup();
const first = app.installBaseView.openInstallBaseModal(customer);
@@ -122,6 +158,8 @@ test("modal shows skeleton, renders only requested fields safely and ignores clo
assert.equal(cells[10].textContent, "0.00");
assert.ok(cells[8].textContent.includes("2026"));
current.listeners.keydown({ key: "Tab", preventDefault() {} });
assert.equal(document.activeElement.type, "search");
current.listeners.keydown({ key: "Tab", preventDefault() {} });
assert.equal(document.activeElement.attributes.role, "region");
assert.equal(app.installBaseView.closeInstallBaseModal(), true);
second.render([], "Late error");

View File

@@ -158,6 +158,6 @@ test("table styles reserve compact widths, prioritize Name/Customer and keep nes
assert.ok(remaining > 0);
assert.ok(remaining / 2 >= 12 + 84, "three customer controls fit after the left inset");
}
const allCss = ["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"].map(read).join("\n");
const allCss = ["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"].map(read).join("\n");
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(allCss)};`));
});

View File

@@ -143,6 +143,35 @@ function setup() {
}
const customer = { CustomerAccount: { PartyNumber: "customer-test", PartyUniqueName: "Synthetic customer" } };
test("service request search covers nested and formatted columns and preserves errors and full results on clearing", () => {
const { app, overlay } = setup();
const view = app.serviceRequestsView.openServiceRequestsModal(customer);
const modal = overlay.children[0];
const input = walk(modal).find(node => node.tag === "input");
const row = { customer: { name: "S\u00e3o Customer", registryId: "REG-123" }, id: "SR-456", severity: "2", status: "Open",
subStatus: "Work In Progress", component: { productName: "Database Alpha" }, os: "Linux", csi: "CSI-789",
created: "2026-03-12", lastUpdated: "2026-04-13", hiddenField: "secretword" };
input.value = "SAO linux"; input.listeners.input();
view.render([row, { id: "Other" }]);
const rows = () => walk(modal).filter(node => node.tag === "td").length / 11;
assert.equal(rows(), 1);
for (const query of ["sao", "REG-123", "SR-456", "2", "open", "progress", "alpha", "linux", "CSI-789", "12/03/2026", "13/04/2026", "2026-03-12"]) {
input.value = query; input.listeners.input();
assert.equal(rows(), 1, query);
}
input.value = "secretword"; input.listeners.input();
assert.equal(rows(), 0);
assert.ok(walk(modal).some(node => node.textContent.includes("No matching records.")));
input.value = ""; input.listeners.input();
assert.equal(rows(), 2);
view.render([], "Synthetic failure");
input.value = "anything"; input.listeners.input();
assert.ok(walk(modal).some(node => node.textContent === "Synthetic failure"));
app.serviceRequestsView.closeServiceRequestsModal();
app.serviceRequestsView.openServiceRequestsModal(customer).render([row]);
assert.equal(walk(overlay).filter(node => node.tag === "td").length, 11);
});
test("skeleton, exact field mapping, safe text, dates, badge, empty/error and focus states", () => {
const { app, overlay, document } = setup();
const trigger = new Element("button", document);
@@ -169,6 +198,8 @@ test("skeleton, exact field mapping, safe text, dates, badge, empty/error and fo
assert.equal(cells[9].textContent, app.opportunitiesFormatters.formatOracleResponseDate("2026-03-04"));
assert.equal(cells[10].textContent, app.opportunitiesFormatters.formatOracleResponseDate("2026-09-17"));
modal.listeners.keydown({ key: "Tab", preventDefault() {} });
assert.equal(document.activeElement.type, "search");
modal.listeners.keydown({ key: "Tab", preventDefault() {} });
assert.equal(document.activeElement.attributes.role, "region");
view.render([]);
assert.ok(walk(modal).some(node => node.textContent === "No open service requests found for this customer."));