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

114 lines
6.1 KiB
JavaScript

const assert = require("node:assert/strict");
const { test } = require("node:test");
const fs = require("node:fs");
const path = require("node:path");
const { loadApplication, composeScript } = require("./support/application.cjs");
const root = path.join(__dirname, "..");
class Element {
constructor(tag) {
this.tag = tag; this.children = []; this.attributes = {}; this.listeners = {}; this.text = "";
this.classList = { toggle() {} };
}
set textContent(value) { this.text = String(value); this.children = []; }
get textContent() { return this.text + this.children.map(child => child.textContent).join(""); }
append(...children) { this.children.push(...children); }
setAttribute(key, value) { this.attributes[key] = value; }
addEventListener(key, callback) { this.listeners[key] = callback; }
}
function setup() {
const app = loadApplication("content", { document: { createElement: tag => new Element(tag), getElementById: () => null,
createElementNS: (namespace, tag) => Object.assign(new Element(tag), { namespace }) } });
app.currentUser.allowsCrmActivities = () => true;
app.productsContentJobs.queueProductsTableRender = () => {};
app.activityProgress.update = () => {};
app.authSessionState.principal = "me@example.invalid";
return app;
}
function checkIcon(badge, kind, value) {
assert.equal(badge.textContent, value);
const images = badge.children.filter(child => child.tag === "svg");
assert.equal(images.length, 1);
const icon = images[0];
assert.equal(icon.namespace, "http://www.w3.org/2000/svg");
assert.equal(icon.attributes.viewBox, "0 0 24 24");
assert.equal(icon.attributes.fill, "none");
assert.equal(icon.attributes.stroke, "currentColor");
assert.equal(icon.attributes["stroke-width"], "2.25");
assert.equal(icon.attributes["stroke-linecap"], "round");
assert.equal(icon.attributes["stroke-linejoin"], "round");
assert.equal(icon.attributes["data-badge-icon"], kind);
assert.equal(icon.attributes["aria-hidden"], "true");
assert.equal(icon.attributes.focusable, "false");
assert.equal(icon.attributes.width, "16");
assert.equal(icon.attributes.height, "16");
assert.ok(icon.children.length >= 3);
assert.ok(icon.children.every(child => ["path", "rect", "polyline"].includes(child.tag)));
assert.ok(icon.children.every(child => child.namespace === icon.namespace));
assert.equal(badge.children.some(child => child.tag === "img"), false);
}
test("consistent inline vectors replace raster icons without parentheses or lost tooltips", async () => {
const app = setup();
const products = { items: [1, 2, 3].map(RampMonths_c => ({ RampMonths_c, RevnId: "123" })) };
const count = app.productsDetails.createProductsCountButton("TEST", products);
checkIcon(count, "products", "3");
assert.equal(count.title, "3 products");
assert.equal(typeof count.listeners.click, "function");
const ramp = app.productsDetails.createProductsRampBadge(products);
checkIcon(ramp, "ramp", "1,2,3");
assert.equal(ramp.title, "Ramp Months: 1, 2, 3");
const missingRamp = app.productsDetails.createProductsRampBadge({ items: [{ RampMonths_c: null }] });
checkIcon(missingRamp, "ramp", "N/D");
assert.equal(missingRamp.attributes["data-missing"], "true");
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [
{ SrNumber: "SR-TEST", ReportedHours_c: 4, OpportunityName_Id_c: { OptyNumber: "TEST" } }
];
await app.opportunityCrmServiceRequests.load("synthetic", app.opportunitiesQueryState.periodRequestVersion);
const sr = app.opportunityCrmServiceRequests.createBadge("TEST");
checkIcon(sr, "sr", "4h");
assert.equal(sr.title, "Whether a Service Request has been created for the opportunities.");
assert.equal(typeof sr.listeners.click, "function");
checkIcon(app.opportunityCrmServiceRequests.createBadge("OTHER"), "sr", "0h");
app.productsContentState.opptyProductsByNumber.set("TEST", products);
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } });
app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: true, hasNext: false, actions: [
{ id: 1, owner: "me@example.invalid", name: "My action" },
{ id: 2, owner: "other@example.invalid", name: "Other action" }
] });
await app.opportunityWorkloadActions.loadProducts(products);
const actions = app.opportunityWorkloadActions.createBadge("TEST");
checkIcon(actions, "actions", "2");
assert.equal(actions.attributes["data-state"], "matched");
assert.match(actions.title, /My action/);
assert.match(actions.title, /Other action/);
app.currentUser.allowsCrmActivities = () => false;
assert.equal(app.opportunityWorkloadActions.createBadge("TEST"), null);
assert.equal(app.opportunityCrmServiceRequests.createBadge("TEST"), null);
});
test("vector shapes and their license ship in both distributions without raster images or requests", () => {
const source = composeScript("content");
const module = fs.readFileSync(path.join(root, "src/features/opportunities/badge-content.js"), "utf8");
assert.ok(source.includes(module));
assert.doesNotMatch(source, /opportunityBadgeImages|data:image\/png;base64/);
for (const target of ["chromium", "firefox"]) {
const bundle = fs.readFileSync(path.join(root, "dist", target, "content.js"), "utf8");
assert.ok(bundle.includes(module));
assert.ok(bundle.includes("Copyright (c) 2026 Lucide Icons and Contributors"));
}
});
test("icon styles preserve uniform dimensions and provide light, dark and active contrast", () => {
const css = fs.readFileSync(path.join(root, "src/features/opportunities/styles.css"), "utf8");
const icon = css.match(/\.opportunities-extension-badge-icon \{([^}]+)\}/)[1];
for (const rule of ["order: -1;", "flex: 0 0 16px;", "width: 16px;", "height: 16px;", "color: inherit;", "stroke: currentColor;", "stroke-width: 2.25;"]) {
assert.ok(icon.includes(rule), rule);
}
assert.doesNotMatch(icon, /filter:|mix-blend-mode:/);
assert.match(css, /gap: 4px;\s*height: 20px;/);
assert.match(css, /cell-actions > \.opportunities-extension-crm-sr-badge \{\s*display: inline-flex;\s*align-items: center;\s*justify-content: flex-start;/);
});