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(customer.children[1].children[1].title, "View customer installed base"); assert.equal(customer.children[1].children[2].title, "View customer service requests"); 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, /min-width: 84px/); assert.match(css, /flex: 0 0 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); 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", "src/features/opportunities/customer-table-search.css"].map(read).join("\n"); assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(allCss)};`)); });