const assert = require("node:assert/strict"); const { test } = require("node:test"); const { loadApplication } = require("./support/application.cjs"); const flush = () => new Promise(resolve => setImmediate(resolve)); function deferred() { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; } for (const first of ["SR", "Actions"]) { test(`Last update waits for every background call when ${first} finishes first`, async () => { const h = setup(); const sr = deferred(), actions = deferred(); let renders = 0; h.app.productsContentJobs.renderProductsProgress = () => { renders += 1; }; h.app.productsContentState.productsLastCachedAt = 100; h.app.opportunityCrmServiceRequests.load = () => sr.promise; h.app.opportunityWorkloadActions.loadProducts = () => actions.promise; const id = h.start(); h.progress(id, { state: "running", optyNumber: "A", items: [{ RevnId: "1" }], cachedAt: 200 }); assert.equal(h.app.productsContentState.productsLastCachedAt, 100); h.progress(id, { state: "complete" }); await flush(); assert.equal(h.app.productsContentState.productsLastCachedAt, 100); (first === "SR" ? sr : actions).resolve(); await flush(); assert.equal(h.app.productsContentState.productsLastCachedAt, 100); (first === "SR" ? actions : sr).resolve(); await flush(); assert.equal(h.app.productsContentState.productsLastCachedAt, 200); assert.equal(renders, 1, "publish the completed cycle once"); }); } test("Last update ignores superseded cycles and settles failures without changing the cache timestamp", async () => { const h = setup(); const old = deferred(), current = deferred(); h.app.productsContentState.productsLastCachedAt = 100; h.app.opportunityCrmServiceRequests.load = () => old.promise; const first = h.start(); h.progress(first, { state: "complete", optyNumber: "A", items: [], cachedAt: 200 }); h.app.opportunityCrmServiceRequests.load = () => current.promise; const second = h.start(); h.progress(second, { state: "complete", optyNumber: "A", items: [], cachedAt: 300 }); old.resolve(); await flush(); assert.equal(h.app.productsContentState.productsLastCachedAt, 100); current.reject(new Error("Synthetic failure")); await flush(); assert.equal(h.app.productsContentState.productsLastCachedAt, 100); assert.equal(h.calls.includes("empty products"), false, "SR failure blocks Actions"); }); test("Last update waits for cache restoration and ignores a superseded Reference", async () => { const h = setup(); const restore = deferred(); h.app.productsContentState.productsLastCachedAt = 100; h.app.opportunityCrmServiceRequests.load = () => restore.promise; const id = h.start(); h.progress(id, { state: "complete", optyNumber: "A", items: [], cachedAt: 200, fromCache: true }); await flush(); assert.equal(h.app.productsContentState.productsLastCachedAt, 100); h.app.opportunitiesQueryState.periodRequestVersion += 1; restore.resolve(); await flush(); assert.equal(h.app.productsContentState.productsLastCachedAt, 100); const next = h.start(); h.progress(next, { state: "complete", optyNumber: "A", items: [], cachedAt: 200, fromCache: true }); await flush(); assert.equal(h.app.productsContentState.productsLastCachedAt, 200); }); test("Last update completes for SALES_REP without activity requests", async () => { const h = setup(); h.app.opportunityCrmServiceRequests.load = () => undefined; h.app.opportunityWorkloadActions.loadProducts = () => undefined; const id = h.start(); h.progress(id, { state: "complete", optyNumber: "A", items: [], cachedAt: 200 }); await flush(); assert.equal(h.app.productsContentState.productsLastCachedAt, 200); }); function setup() { let listener; const app = loadApplication("content", { document: { getElementById: () => null }, window: { requestAnimationFrame() {} }, chrome: { runtime: { onMessage: { addListener(fn) { listener = fn; } } } } }); const calls = []; app.opportunityCrmServiceRequests.reset = () => calls.push("reset SR"); app.opportunityWorkloadActions.reset = () => calls.push("reset Act"); app.opportunityCrmServiceRequests.load = async (token, version, options) => { if (!options?.cacheOnly) calls.push(`SR ${token}`); }; app.opportunityWorkloadActions.loadProducts = (products, refresh, options) => { if (!options?.cacheOnly) calls.push(products.items[0]?.RevnId || "empty products"); }; app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: true }); app.productsContentJobs.ensureProductsBackgroundMessageHandler(); function start(token = "token", numbers = ["A"]) { app.productsContentJobs.requestOpptyProductsInBackground(numbers.map(OptyNumber => ({ OptyNumber })), token); return app.productsContentState.currentProductsRequestId; } function progress(requestId, data) { listener({ type: "opportunitiesExtension.opptyProductsProgress", requestId, ...data }); } return { app, calls, start, progress }; } test("follow-up calls wait for completion, deduplicate terminal messages and refresh on every new cycle", async () => { const h = setup(); const first = h.start(); h.progress(first, { state: "running", optyNumber: "A", items: [{ RevnId: "123" }], fromCache: false }); assert.deepEqual(h.calls, []); h.progress(first, { state: "complete" }); h.progress(first, { state: "complete" }); await flush(); assert.deepEqual(h.calls, ["SR token", "123"]); const second = h.start("new-token"); h.progress(first, { state: "complete", optyNumber: "B", items: [{ RevnId: "old" }] }); h.progress(second, { state: "running", optyNumber: "A", items: [{ RevnId: "123" }] }); assert.equal(h.calls.filter(value => value === "123").length, 1); h.progress(second, { state: "complete" }); await flush(); assert.equal(h.calls.filter(value => value === "123").length, 2); assert.ok(h.calls.includes("SR new-token")); assert.equal(h.calls.includes("old"), false); }); test("partial failures use successful products only; closed, failed and empty cycles have no follow-up", async () => { const h = setup(); const first = h.start(); h.progress(first, { optyNumber: "A", items: [{ RevnId: "123" }] }); h.progress(first, { optyNumber: "B", items: [{ RevnId: "failed" }], error: "Synthetic failure" }); h.progress(first, { state: "complete-with-errors" }); await flush(); assert.equal(h.calls.includes("failed"), false); assert.equal(h.calls.filter(value => value === "SR token").length, 1); const second = h.start(); h.progress(second, { optyNumber: "A", error: "Synthetic failure" }); h.progress(second, { state: "complete-with-errors" }); const third = h.start(); h.progress(third, { state: "error" }); const fourth = h.start(); h.app.productsContentState.currentProductsRequestId = ""; h.progress(fourth, { state: "complete", optyNumber: "A", items: [{ RevnId: "closed" }] }); h.app.productsContentJobs.requestOpptyProductsInBackground([], "empty"); await flush(); assert.equal(h.calls.filter(value => value.startsWith("SR ")).length, 1); assert.equal(h.calls.includes("closed"), false); }); test("cached and mixed product cycles both allow activity cache misses to be fetched", async () => { const h = setup(); const cached = h.start(); h.progress(cached, { optyNumber: "A", items: [{ RevnId: "cached" }], fromCache: true }); h.progress(cached, { state: "complete" }); await flush(); assert.deepEqual(h.calls, ["SR token", "cached"]); h.calls.length = 0; const mixed = h.start("token", ["A", "B"]); h.progress(mixed, { optyNumber: "A", items: [{ RevnId: "cached" }], fromCache: true }); h.progress(mixed, { optyNumber: "B", items: [{ RevnId: "fresh" }], fromCache: false }); h.progress(mixed, { state: "complete" }); await flush(); assert.deepEqual(h.calls, ["SR token", "cached", "fresh"]); }); test("activity scope includes hidden reference opportunities and excludes entries from other references", async () => { const h = setup(); h.app.opportunitiesFilterState.tableSearch = "hide everything"; const id = h.start("token", ["A", "B"]); h.progress(id, { optyNumber: "A", items: [{ RevnId: "1" }, { RevnId: "2" }] }); h.progress(id, { optyNumber: "B", items: [{ RevnId: "3" }] }); h.progress(id, { optyNumber: "OTHER_REFERENCE", items: [{ RevnId: "outside" }] }); h.progress(id, { state: "complete" }); await flush(); assert.deepEqual(h.calls, ["SR token", "1", "3"]); assert.equal(h.calls.includes("outside"), false); }); test("reopening an existing modal does not reload products or reset/query activities", async () => { const modal = { hidden: true, querySelector: () => ({ focus() {} }) }; const app = loadApplication("content", { window: {}, document: { getElementById: id => id === "opportunities-extension-modal" ? modal : null, documentElement: { classList: { add() {} } }, addEventListener() {} } }); app.contentStyles.ensureExtensionStyles = () => {}; app.currentUser.ensure = async () => true; app.authContentSession.requestTokenRelayOnce = async () => "synthetic"; app.opportunitiesTable.renderOpportunitiesTable = () => {}; const calls = []; app.productsContentJobs.requestOpptyProductsInBackground = () => calls.push("products"); app.opportunityCrmServiceRequests.load = () => calls.push("SR"); app.opportunityCrmServiceRequests.reset = () => calls.push("reset SR"); app.opportunityWorkloadActions.loadProducts = () => calls.push("Actions"); app.opportunityWorkloadActions.reset = () => calls.push("reset Actions"); app.contentModal.openOpportunitiesModal(); await flush(); assert.equal(modal.hidden, false); assert.deepEqual(calls, []); });