const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
const model = loadApplication("content").rampProjection;
const product = (workload = {}, overrides = {}) => ({ RampMonths_c: 2, RevnAmount: 1,
ConsumptionStartDate_c: "2000-01-01", workload: { adjustedACR: 50000,
consumptionStartDate: "2026-11-01T00:00:00", forecast: [{ month: 11, year: 2026, adjustedConsumptionAmount: 1515.15 }],
...workload }, ...overrides });
test("ramp formula agrees with all twelve workbook ramps and preserves the annual total", () => {
// rampas!E6:E17 and the stabilized values in P6:P17, annual value F2 = 50000.
const first = [4166.666666666667, 2173.913043478261, 1515.151515151515, 1190.4761904761904, 1000,
877.1929824561404, 793.6507936507936, 735.2941176470588, 694.4444444444445, 666.6666666666667,
649.3506493506494, 641.025641025641];
const stable = [4166.666666666667, 4347.826086956522, 4545.454545454545, 4761.9047619047615,
5000, 5263.1578947368425, 5555.555555555556, 5882.35294117647, 6250, 6666.666666666667,
7142.857142857143, 7692.307692307692];
for (let ramp = 0; ramp <= 11; ramp++) {
assert.ok(Math.abs(model.monthlyAmount(50000, ramp, 1) - first[ramp]) < 1e-9);
assert.ok(Math.abs(model.monthlyAmount(50000, ramp, 12) - stable[ramp]) < 1e-9);
const months = Array.from({ length: 12 }, (_, i) => model.monthlyAmount(50000, ramp, i + 1));
assert.ok(Math.abs(months.reduce((sum, value) => sum + value, 0) - 50000) < 1e-9);
assert.equal(model.monthlyAmount(50000, ramp, 13), months[11], "no annual restart");
assert.equal(model.monthlyAmount(50000, ramp, 0), 0);
}
});
test("comparison uses adjustedACR, product RampMonths_c, calendar months and cent precision", () => {
const result = model.compare(product({ rampMonths: 0, actualRampMonths: 11, forecast: [
{ month: 1, year: 2027, adjustedConsumptionAmount: 4500 },
{ month: 11, year: 2026, adjustedConsumptionAmount: "1515.151515" },
{ month: 12, year: 2026, adjustedConsumptionAmount: 3200 },
{ month: 10, year: 2026, adjustedConsumptionAmount: 0 }
] }));
assert.equal(result.status, "divergent");
assert.equal(result.divergentCount, 2);
assert.deepEqual(Array.from(result.rows, row => [row.key, row.calculated, row.difference]), [
["2026-10", 0, 0], ["2026-11", 1515.15, 0], ["2026-12", 3030.30, 169.70], ["2027-01", 4545.45, -45.45]
]);
assert.equal(model.compare(product()).status, "matched");
});
test("first month is prorated inclusively without timezone shifts, including leap years", () => {
for (const [start, month, year, expected, days, total] of [
["2026-11-25T00:00:00-0300", 11, 2026, 303.03, 6, 30],
["2026-11-30T00:00:00Z", 11, 2026, 50.51, 1, 30],
["2028-02-29T00:00:00+1400", 2, 2028, 52.25, 1, 29],
["2027-02-28", 2, 2027, 54.11, 1, 28],
["2026-12-01", 12, 2026, 1515.15, 31, 31]
]) {
const result = model.compare(product({ consumptionStartDate: start,
forecast: [{ month, year, adjustedConsumptionAmount: expected }] }));
assert.equal(result.rows[0].calculated, expected, start);
assert.equal(result.status, "matched", start);
assert.equal(result.firstMonthDays, days);
assert.equal(result.firstMonthTotalDays, total);
}
});
test("missing or invalid inputs never create false mismatches or silently default to zero", () => {
for (const value of [null, "", "garbage", -1, Infinity]) assert.equal(model.compare(product({ adjustedACR: value })).status, "unavailable");
for (const value of [null, "", "garbage", -1, 12, 1.5]) assert.equal(model.compare(product({}, { RampMonths_c: value })).status, "unavailable");
for (const value of [null, "", "2026-02-30", "2026-13-01"]) assert.equal(model.compare(product({ consumptionStartDate: value })).status, "unavailable");
assert.equal(model.compare({}).status, "unavailable");
for (const forecast of [[], null, [{ year: 2026, month: 0 }], [{ year: 2026, month: 11 }, { year: 2026, month: 11 }]]) {
assert.equal(model.compare(product({ forecast })).status, "unavailable");
}
assert.equal(model.compare(product({ adjustedACR: 0, forecast: [{ year: 2026, month: 11, adjustedConsumptionAmount: 0 }] }, { RampMonths_c: "0" })).status, "matched");
const missing = model.compare(product({ forecast: [{ year: 2026, month: 11, projectedConsumptionAmount: 500 }] }));
assert.equal(missing.status, "unavailable");
assert.equal(missing.rows[0].actual, null);
assert.equal(missing.rows[0].difference, null);
});
test("calculation is read-only and keeps missing monthly values visible beside real differences", () => {
const input = product({ forecast: [{ year: 2026, month: 11 }, { year: 2026, month: 12, adjustedConsumptionAmount: 0 }] });
const before = JSON.stringify(input);
const result = model.compare(input);
assert.equal(JSON.stringify(input), before);
assert.equal(result.status, "divergent");
assert.equal(result.divergentCount, 1);
assert.equal(result.missingCount, 1);
assert.ok(result.reason);
});
function dom() {
const document = { activeElement: null };
class Element {
constructor(tag) { this.tag = tag; this.children = []; this.attributes = {}; this.dataset = {}; this.listeners = {}; this.classList = { toggle() {} }; this.inert = false; this.isConnected = true; }
set textContent(value) { this.text = String(value); this.children = []; }
get textContent() { return (this.text || "") + this.children.map(child => child.textContent).join(""); }
append(...children) { for (const child of children) { child.parent = this; this.children.push(child); } }
replaceChildren(...children) { this.children = []; this.append(...children); }
querySelectorAll(selector) { return walk(this).filter(node => node !== this && node.tag === selector); }
setAttribute(name, value) { this.attributes[name] = value; }
addEventListener(name, fn) { this.listeners[name] = fn; }
focus() { document.activeElement = this; }
remove() { this.parent.children = this.parent.children.filter(child => child !== this); this.isConnected = false; }
click() { this.listeners.click?.({ preventDefault() {}, stopPropagation() {} }); }
}
const host = new Element("main"), content = new Element("div");
host.append(content);
document.createElement = tag => new Element(tag);
document.createElementNS = (_, tag) => new Element(tag);
document.getElementById = () => host;
return { document, host, content };
}
const walk = root => [root, ...root.children.flatMap(walk)];
test("Update estimates prevents duplicate clicks, reports errors, retries and closes on success", async () => {
const { document, host } = dom();
const app = loadApplication("content", { document, Intl, Date });
const input = product({ forecast: [{ year: 2026, month: 11, adjustedConsumptionAmount: 0 }] });
const notifications = [];
app.opportunitiesLinks.showCopyConfirmation = (...args) => notifications.push(args);
let reject, calls = 0;
app.workloadForecastUpdate.save = () => { calls++; return new Promise((_, fail) => { reject = fail; }); };
app.rampProjectionView.open(input, { OptyNumber: "TEST" });
const modal = host.children[1];
const button = walk(modal).find(node => node.textContent === "Update estimates");
const event = { preventDefault() {}, stopPropagation() {} };
button.listeners.click(event);
assert.equal(calls, 0, "opening confirmation must not save");
const confirmation = () => walk(modal).find(node => node.attributes.role === "alertdialog");
assert.ok(confirmation().textContent.includes("The estimates will be updated directly in the SPA"));
const cancel = walk(confirmation()).find(node => node.textContent === "Cancel");
assert.equal(document.activeElement, cancel);
cancel.click();
assert.equal(confirmation(), undefined);
assert.equal(document.activeElement, button);
assert.equal(calls, 0, "cancelling must not save");
button.click();
const pending = walk(confirmation()).find(node => node.textContent === "Confirm update").listeners.click(event);
button.click();
assert.equal(confirmation(), undefined);
assert.equal(calls, 1);
assert.equal(button.disabled, true);
assert.equal(app.rampProjectionView.close(), true);
assert.equal(host.children.length, 2, "cannot dismiss while saving");
reject(new Error("Forbidden"));
await pending;
assert.equal(button.disabled, false);
assert.deepEqual(notifications, [["Forbidden", true]]);
app.workloadForecastUpdate.save = async () => { calls++; };
button.click();
await walk(confirmation()).find(node => node.textContent === "Confirm update").listeners.click(event);
assert.equal(host.children.length, 1);
assert.deepEqual(notifications[1], ["Workload estimates updated successfully."]);
});
test("single-product opportunity alert opens the comparison directly with opportunity context", () => {
const { document, host, content } = dom();
const app = loadApplication("content", { document, Intl, Date });
const opportunity = { OptyNumber: "ONLY", CustomerAccount: { PartyUniqueName: "Single customer" } };
const input = product({ forecast: [{ year: 2026, month: 11, adjustedConsumptionAmount: 0 }] });
let expansions = 0;
app.productsDetails.expandProducts = () => { expansions++; };
const alert = app.rampProjectionView.createOpportunityIndicator({ items: [input] }, opportunity, false);
content.append(alert);
assert.equal(alert.attributes["aria-haspopup"], "dialog");
assert.equal(alert.attributes["aria-expanded"], undefined);
assert.match(alert.title, /View comparison/);
alert.click();
assert.equal(expansions, 0);
assert.equal(host.children[1].attributes.role, "dialog");
assert.ok(host.children[1].textContent.includes("ONLY"));
assert.ok(host.children[1].textContent.includes("Single customer"));
app.rampProjectionView.close();
assert.equal(document.activeElement, alert);
const multiple = app.rampProjectionView.createOpportunityIndicator({ items: [input, product()] }, opportunity, false);
assert.equal(multiple.attributes["aria-haspopup"], undefined);
multiple.click();
assert.equal(expansions, 1, "two products still expand even when only one is divergent");
assert.equal(host.children.length, 1);
});
test("projection sorting restores on reopening without changing forecast source order", () => {
const { document, host } = dom();
const app = loadApplication("content", { document, Intl, Date });
const input = product({ forecast: [
{ year: 2026, month: 11, adjustedConsumptionAmount: 10 },
{ year: 2026, month: 12, adjustedConsumptionAmount: 20 }
] });
const before = JSON.stringify(input);
const opportunity = { OptyNumber: "TEST" };
app.rampProjectionView.open(input, opportunity);
const button = walk(host).find(node => node.attributes["data-sort-key"] === "month");
button.click(); button.click();
const firstMonth = () => walk(host).find(node => node.tag === "tbody").children[0].children[0].textContent;
const expected = firstMonth();
app.rampProjectionView.close(); app.rampProjectionView.open(input, opportunity);
assert.equal(firstMonth(), expected);
assert.equal(walk(host).find(node => node.attributes["data-sort-key"] === "month").parent.attributes["aria-sort"], "descending");
assert.equal(JSON.stringify(input), before);
});
test("product column opens accessible read-only comparison and restores focus/background on close", () => {
const { document, host, content } = dom();
const app = loadApplication("content", { document, Intl, Date });
const input = product({ consumptionStartDate: "2026-11-25", forecast: [{ year: 2026, month: 11, adjustedConsumptionAmount: 1000 }] },
{ RevnId: 1, ProdGroupName: "Product", RevnAmountCurcyCode: "USD" });
app.productsContentState.opptyProductsByNumber.set("TEST", { items: [input] });
const row = app.productsDetails.createProductsDetailRow({ OptyNumber: "TEST", CustomerAccount: { PartyUniqueName: "Example" } }, 10);
const alert = walk(row).find(node => node.attributes["aria-haspopup"] === "dialog");
assert.ok(walk(row).some(node => node.tag === "th" && node.textContent === "Projection"));
assert.ok(alert);
assert.equal(alert.children[0].tag, "svg");
content.append(row);
alert.click();
const modal = host.children[1];
assert.equal(modal.attributes.role, "dialog");
assert.equal(content.inert, true);
const elements = walk(modal);
for (const text of ["Forecast", "Calculated", "Difference", "11/2026 (Prorated)", "$303.03", "$696.97"]) {
assert.ok(elements.some(node => node.textContent === text), text);
}
assert.ok(elements.some(node => node.textContent === "Product"));
assert.equal(elements.some(node => node.tag === "b"), false, "names use safe text nodes");
const closeButton = elements.find(node => node.title === "Close consumption projection");
assert.equal(document.activeElement, closeButton);
let prevented = false;
modal.listeners.keydown({ key: "Tab", shiftKey: true, preventDefault() { prevented = true; } });
assert.equal(prevented, true);
assert.equal(document.activeElement.textContent, "Update estimates");
modal.listeners.keydown({ key: "Escape", preventDefault() {}, stopPropagation() {} });
assert.equal(host.children.length, 1);
assert.equal(content.inert, false);
assert.equal(document.activeElement, alert);
assert.equal(app.rampProjectionView.createIndicator(product(), {}).textContent, "OK");
assert.equal(app.rampProjectionView.createIndicator({}, {}).disabled, true);
app.rampProjectionView.open(product(), {}, alert);
assert.ok(walk(host).some(node => node.textContent === "0 months with a difference"));
app.rampProjectionView.close();
});