Table sorting preferences will be saved and reused based on the user’s selections

This commit is contained in:
2026-10-25 00:39:55 -03:00
parent 19edee4e11
commit 0f1f399d1a
40 changed files with 896 additions and 230 deletions

View File

@@ -31,11 +31,11 @@ const synthetic = (number = "TEST-1", name = "Synthetic opportunity") => ({
EffectiveDate: "2026-10-23", LastUpdateDate: "2026-09-22T13:15:00Z", StatusCode: "OPEN", ForecastGroup_c: "FORECAST"
});
function setup(items = [synthetic()]) {
function setup(items = [synthetic()], localStorage) {
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 app = loadApplication("content", { document, Intl, Date, localStorage });
app.currentUser.allowsCrmActivities = () => true;
const summaries = [];
app.aiSummaryContent.requestGenerateAiSummary = number => summaries.push(number);
@@ -46,6 +46,22 @@ function setup(items = [synthetic()]) {
return { app, table, rows, cell, headers, summaries };
}
test("Overview restores independent column and direction preferences across reloads and new data", () => {
const saved = new Map();
const storage = { getItem: key => saved.get(key), setItem: (key, value) => saved.set(key, value) };
const items = [synthetic("B", "Beta"), synthetic("A", "Alpha")];
const first = setup(items, storage);
first.app.opportunitiesTable.sortOpportunitiesBy("name");
first.app.opportunitiesTable.sortOpportunitiesBy("name");
const second = setup(items.slice().reverse(), storage);
assert.equal(second.app.opportunitiesTableState.opportunitiesTableState.sortKey, "name");
assert.equal(second.app.opportunitiesTableState.opportunitiesTableState.sortDirection, "descending");
assert.equal(second.cell("name").children[0].children[0].textContent, "Beta");
second.app.opportunitiesTable.setOpportunitiesTableState({ status: "loading" });
second.app.opportunitiesTable.setOpportunitiesTableState({ status: "ready", items });
assert.equal(second.cell("name").children[0].children[0].textContent, "Beta");
});
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" }] });
@@ -83,8 +99,8 @@ test("ramp months preserve zero and flag missing values beside the product count
const badge = () => findClass(h.cell("name"), "opportunities-extension-products-ramp");
assert.equal(badge(), undefined);
for (const [values, expected] of [
[[], null], [[null], "N/D"], [[null, undefined, "", " "], "N/D"], [[6], "6"],
[[0], "0"], [[0, "3", null, " 6 ", 3], "0 | 3 | N/D | 6 | 3"]
[[], null], [[null], "N/A"], [[null, undefined, "", " "], "N/A"], [[6], "6"],
[[0], "0"], [[0, "3", null, " 6 ", 3], "0 | 3 | N/A | 6 | 3"]
]) {
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", {
fromCache: true, items: values.map(RampMonths_c => ({ RampMonths_c }))
@@ -93,7 +109,7 @@ test("ramp months preserve zero and flag missing values beside the product count
if (expected === null) assert.equal(badge(), undefined);
else {
assert.equal(badge().textContent, expected);
assert.equal(badge().attributes["data-missing"], String(expected.includes("N/D")));
assert.equal(badge().attributes["data-missing"], String(expected.includes("N/A")));
assert.equal(badge().title, `Ramp Months: ${expected.split(" | ").join(", ")}`);
assert.equal(badge().attributes["aria-label"], badge().title);
assert.equal(h.cell("name").children[0].children[1].children[1], badge());
@@ -200,7 +216,7 @@ test("product table distinguishes unavailable ramp months from numeric zero", ()
findClass(h.cell("name"), "opportunities-extension-products-count").click();
const table = findClass(h.rows()[1], "opportunities-extension-products-table");
const rows = table.children.find(child => child.tag === "tbody").children;
assert.deepEqual(rows.map(row => row.children[9].textContent), ["N/D", "N/D", "N/D", "N/D", "0", "0", "6"]);
assert.deepEqual(rows.map(row => row.children[9].textContent), ["N/A", "N/A", "N/A", "N/A", "0", "0", "6"]);
});
test("customer consumption still opens immediately, uses the right customer and preserves loading state", async () => {
@@ -239,9 +255,31 @@ test("pending, zero-count, failed and missing-data rows preserve their layout an
assert.equal(h.cell("customer").children[0].children[0].textContent, "-");
});
test("loading, empty and error messages span exactly the visible columns", () => {
test("loading skeleton preserves columns and action slots, then disappears on success or failure", () => {
const h = setup();
for (const status of ["loading", "empty", "error"]) {
for (const nextStatus of ["ready", "empty", "error"]) {
h.app.opportunitiesTable.setOpportunitiesTableState({ status: "loading", items: [], message: "Loading opportunities..." });
assert.equal(h.table.attributes["aria-busy"], "true");
assert.equal(h.rows().length, 8);
assert.equal(h.headers().length, 10);
assert.ok(h.headers().every(header => header.children[0].disabled));
for (const row of h.rows()) {
assert.equal(row.attributes["aria-hidden"], "true");
assert.deepEqual(row.children.map(cell => cell.attributes["data-column"]), h.headers().map(header => header.attributes["data-column"]));
assert.ok(row.children.every(cell => findClass(cell, "opportunities-extension-account-plans-skeleton-bar")));
assert.equal(walk(row).filter(node => hasClass(node, "opportunities-extension-table-skeleton-badge")).length, 7);
assert.equal(walk(row).some(node => node.tag === "button" || node.tag === "a"), false);
}
h.app.opportunitiesTable.setOpportunitiesTableState({ status: nextStatus, items: nextStatus === "ready" ? [synthetic()] : [], message: "" });
assert.equal(h.table.attributes["aria-busy"], "false");
assert.equal(findClass(h.table, "opportunities-extension-table-skeleton-row"), undefined);
assert.ok(h.headers().every(header => !header.children[0].disabled));
}
});
test("empty and error messages span exactly the visible columns", () => {
const h = setup();
for (const status of ["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);