Repositioned the opportunity and customer control buttons
This commit is contained in:
158
tests/opportunityTable.test.cjs
Normal file
158
tests/opportunityTable.test.cjs
Normal file
@@ -0,0 +1,158 @@
|
||||
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");
|
||||
|
||||
class Element {
|
||||
constructor(tag) {
|
||||
this.tag = tag;
|
||||
this.children = [];
|
||||
this.dataset = {};
|
||||
this.attributes = {};
|
||||
this.listeners = {};
|
||||
this.classes = new Set();
|
||||
this.classList = { toggle: (name, on) => on ? this.classes.add(name) : this.classes.delete(name) };
|
||||
this.text = "";
|
||||
}
|
||||
set textContent(value) { this.text = value; this.children = []; }
|
||||
get textContent() { return this.text; }
|
||||
append(...nodes) { this.children.push(...nodes); }
|
||||
setAttribute(key, value) { this.attributes[key] = value; }
|
||||
addEventListener(key, handler) { this.listeners[key] = handler; }
|
||||
click() { if (!this.disabled) this.listeners.click({ preventDefault() {}, stopPropagation() {} }); }
|
||||
}
|
||||
const walk = node => [node, ...node.children.flatMap(walk)];
|
||||
const hasClass = (node, name) => (node.className || "").split(" ").includes(name);
|
||||
const findClass = (node, name) => walk(node).find(child => hasClass(child, name));
|
||||
const synthetic = (number = "TEST-1", name = "Synthetic opportunity") => ({
|
||||
Name: name, OptyNumber: number, CustomerAccount: { PartyUniqueName: "Synthetic customer", PartyId: "party/id", PartyNumber: "reg-123" },
|
||||
Owner: { PersonFirstName: "Test", PersonLastName: "Owner" }, PrimaryRevenue: { RevnAmount: "123456.78", RevnAmountCurcyCode: "USD", WinProb: 60 },
|
||||
EffectiveDate: "2026-10-23", LastUpdateDate: "2026-09-22T13:15:00Z", StatusCode: "OPEN", ForecastGroup_c: "FORECAST"
|
||||
});
|
||||
|
||||
function setup(items = [synthetic()]) {
|
||||
const table = new Element("table");
|
||||
const document = { createElement: tag => new Element(tag), createElementNS: (_, tag) => new Element(tag),
|
||||
getElementById: id => id === app.contracts.OPPORTUNITIES_TABLE_ID ? table : null };
|
||||
const app = loadApplication("content", { document, Intl, Date });
|
||||
const summaries = [];
|
||||
app.aiSummaryContent.requestGenerateAiSummary = number => summaries.push(number);
|
||||
app.opportunitiesTable.setOpportunitiesTableState({ status: "ready", items });
|
||||
const rows = () => table.children.find(child => child.tag === "tbody").children;
|
||||
const cell = (key, index = 0) => rows()[index].children.find(child => child.attributes["data-column"] === key);
|
||||
const headers = () => table.children.find(child => child.tag === "thead").children[0].children;
|
||||
return { app, table, rows, cell, headers, summaries };
|
||||
}
|
||||
|
||||
test("ten columns retain their links and put product/customer controls below the corresponding text", () => {
|
||||
const h = setup();
|
||||
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", { items: [{ ProdGroupName: "Cloud" }] });
|
||||
h.app.opportunitiesTable.renderOpportunitiesTable();
|
||||
assert.equal(h.headers().length, 10);
|
||||
assert.equal(h.headers().some(header => header.attributes["data-column"] === "actions"), false);
|
||||
const name = h.cell("name").children[0];
|
||||
const customer = h.cell("customer").children[0];
|
||||
assert.equal(name.children[0].tag, "a");
|
||||
assert.equal(name.children[0].textContent, "Synthetic opportunity");
|
||||
assert.ok(name.children[0].href.endsWith("TEST-1"));
|
||||
assert.equal(name.children[1].children[0].className, "opportunities-extension-products-count");
|
||||
assert.equal(name.children[1].children[0].textContent, "1");
|
||||
assert.equal(customer.children[0].textContent, "Synthetic customer");
|
||||
assert.ok(customer.children[0].href.endsWith("party%2Fid"));
|
||||
assert.equal(customer.children[1].children[0].title, "View customer consumption");
|
||||
assert.equal(findClass(h.cell("optyNumber"), "opportunities-extension-copy-button").title, "Copy opportunity number");
|
||||
assert.equal(findClass(h.cell("name"), "opportunities-extension-action-button"), undefined);
|
||||
assert.equal(findClass(h.cell("customer"), "opportunities-extension-products-count"), undefined);
|
||||
});
|
||||
|
||||
test("products remain expandable with AI summary and a ten-column detail row after sorting", () => {
|
||||
const h = setup([synthetic("B", "Zulu"), synthetic("A", "Alpha")]);
|
||||
h.app.productsContentState.opptyProductsByNumber.set("B", { items: [{ ProdGroupName: "Cloud", RevnAmount: "10" }] });
|
||||
h.app.opportunitiesTable.renderOpportunitiesTable();
|
||||
findClass(h.cell("name"), "opportunities-extension-products-count").click();
|
||||
assert.deepEqual(h.summaries, ["B"]);
|
||||
assert.equal(h.rows()[1].className, "opportunities-extension-products-detail-row");
|
||||
assert.equal(h.rows()[1].children[0].colSpan, 10);
|
||||
assert.ok(findClass(h.rows()[1], "opportunities-extension-products-table"));
|
||||
h.headers()[0].children[0].click();
|
||||
assert.equal(h.cell("name").children[0].children[0].textContent, "Alpha");
|
||||
assert.equal(h.cell("name", 1).children[0].children[0].textContent, "Zulu");
|
||||
assert.equal(h.rows()[2].children[0].colSpan, 10);
|
||||
findClass(h.cell("name", 1), "opportunities-extension-products-count").click();
|
||||
assert.equal(h.rows().length, 2);
|
||||
assert.deepEqual(h.summaries, ["B"], "collapsing does not generate another summary");
|
||||
});
|
||||
|
||||
test("customer consumption still opens immediately, uses the right customer and preserves loading state", async () => {
|
||||
const h = setup();
|
||||
const opens = [];
|
||||
const requests = [];
|
||||
h.app.consumptionPlansView.openAccountPlansModal = (...args) => opens.push(args);
|
||||
h.app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ synthetic: true });
|
||||
h.app.platformContentMessages.sendRuntimeMessage = async message => { requests.push(message); return { ok: true, plans: [] }; };
|
||||
findClass(h.cell("customer"), "opportunities-extension-action-button").click();
|
||||
assert.equal(opens[0][2], "loading");
|
||||
assert.equal(findClass(h.cell("customer"), "opportunities-extension-action-button").disabled, true);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(requests.length, 1);
|
||||
assert.equal(requests[0].partyNumber, "reg-123");
|
||||
assert.equal(requests[0].type, "opportunitiesExtension.requestAccountPlans");
|
||||
assert.equal(opens.at(-1)[2], "ready");
|
||||
assert.equal(findClass(h.cell("customer"), "opportunities-extension-action-button").disabled, false);
|
||||
});
|
||||
|
||||
test("pending, zero-count, failed and missing-data rows preserve their layout and full values", () => {
|
||||
const h = setup([{ ...synthetic(), StatusCode: "WON_PENDING" }]);
|
||||
const actions = () => h.cell("name").children[0].children[1];
|
||||
assert.equal(actions().children.length, 0, "space remains reserved while products load");
|
||||
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", { items: [], error: "Synthetic failure" });
|
||||
h.app.opportunitiesTable.renderOpportunitiesTable();
|
||||
assert.equal(actions().children[0].textContent, "0");
|
||||
assert.equal(actions().children[0].title, "Synthetic failure");
|
||||
assert.equal(h.cell("status").children[0].textContent, "WON_PENDING");
|
||||
assert.equal(h.cell("revenue").textContent, "$123,456.78");
|
||||
assert.ok(h.cell("lastUpdateDate").textContent.includes("2026"));
|
||||
const missing = { Name: "No identifiers", StatusCode: "OPEN", ForecastGroup_c: "FORECAST" };
|
||||
h.app.opportunitiesFilters.getSortedOpportunities = () => [missing];
|
||||
h.app.opportunitiesTable.setOpportunitiesTableState({ status: "ready", items: [missing] });
|
||||
assert.equal(findClass(h.cell("name"), "opportunities-extension-products-count"), undefined);
|
||||
assert.equal(h.cell("customer").children[0].children[0].textContent, "-");
|
||||
});
|
||||
|
||||
test("loading, empty and error messages span exactly the visible columns", () => {
|
||||
const h = setup();
|
||||
for (const status of ["loading", "empty", "error"]) {
|
||||
h.app.opportunitiesTable.setOpportunitiesTableState({ status, message: "Synthetic state" });
|
||||
assert.equal(h.rows().length, 1);
|
||||
assert.equal(h.rows()[0].children[0].colSpan, 10);
|
||||
}
|
||||
});
|
||||
|
||||
test("table styles reserve compact widths, prioritize Name/Customer and keep nested tables untouched", () => {
|
||||
const read = file => fs.readFileSync(path.join(__dirname, "..", file), "utf8");
|
||||
const css = read("src/features/opportunities/styles.css");
|
||||
const fixed = [...css.matchAll(/th\[data-column="\w+"\] \{ width: (\d+)px;/g)].reduce((sum, match) => sum + Number(match[1]), 0);
|
||||
assert.equal(fixed, 916);
|
||||
const flexible = [...css.matchAll(/th\[data-column="(\w+)"\] \{ width: auto;/g)].map(match => match[1]);
|
||||
assert.deepEqual(flexible, ["name", "customer"]);
|
||||
assert.match(css, /th\[data-column="owner"\] \{ width: 132px;/);
|
||||
assert.doesNotMatch(css, /width: (?:calc|clamp)\(/);
|
||||
assert.match(css, /min-width: 1120px/);
|
||||
assert.match(css, /min-height: 2\.7em/);
|
||||
assert.match(css, /padding-bottom: 38px/);
|
||||
const actions = css.match(/\.opportunities-extension-cell-actions \{([^}]+)\}/)[1];
|
||||
assert.match(actions, /position: absolute/);
|
||||
assert.match(actions, /bottom: 10px/);
|
||||
assert.match(actions, /height: 24px/);
|
||||
assert.match(css, /td\[data-column="customer"\] \{\s*position: relative;/);
|
||||
assert.match(css, /grid-template-columns: minmax\(0, 1fr\) 12px/);
|
||||
assert.doesNotMatch(css, /text-overflow: ellipsis|overflow: hidden|th:nth-child|td:nth-child/);
|
||||
for (const width of [1120, 1280, 1440, 1840]) {
|
||||
const remaining = width - fixed;
|
||||
assert.equal(fixed + remaining / 2 + remaining / 2, width);
|
||||
assert.ok(remaining > 0);
|
||||
}
|
||||
const allCss = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css"].map(read).join("\n");
|
||||
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(allCss)};`));
|
||||
});
|
||||
Reference in New Issue
Block a user