const assert = require("node:assert/strict"); const { test } = require("node:test"); const { loadApplication } = require("./support/application.cjs"); const { loadBackground, response } = require("./support/characterization.cjs"); const flush = () => new Promise(resolve => setImmediate(resolve)); for (const family of ["chrome", "firefox"]) { test(`${family}: invalidation detaches old product flights and prevents stale cache restoration`, async () => { const pending = []; const storage = { preferences: { keep: true } }; const app = loadBackground({ family, storage, fetch: () => new Promise(resolve => pending.push(resolve)) }); const message = { type: "opportunitiesExtension.requestOpptyProducts", accessToken: "synthetic", optyNumbers: ["A"], cacheMaxAgeMs: 86400000 }; const old = app.dispatch({ ...message, requestId: "old" }); await flush(); assert.equal(pending.length, 1); const clear = await app.dispatch({ type: "opportunitiesExtension.invalidateProductsCache" }); assert.equal(clear.ok, true); const fresh = app.dispatch({ ...message, requestId: "fresh" }); await flush(); assert.equal(pending.length, 2); pending[1](response({ items: [{ RevnId: "new" }], count: 1, hasMore: false })); await fresh; pending[0](response({ items: [{ RevnId: "old" }], count: 1, hasMore: false })); await old; await app.dispatch({ ...message, requestId: "cached" }); assert.equal(pending.length, 2); assert.equal(storage["opportunitiesExtension.opptyProductsCache.v1"].A.items[0].RevnId, "new"); assert.deepEqual(storage.preferences, { keep: true }); }); } test("activity invalidation preserves unrelated storage and rejects late entries from old product timestamps", async () => { const values = { preferences: "keep" }; const app = loadApplication("content", { browser: { storage: { local: { get: async key => key === null ? { ...values } : { [key]: values[key] }, set: async data => Object.assign(values, data), remove: async keys => keys.forEach(key => delete values[key]) } } } }); app.authSessionState.principal = "user@example.invalid"; const stamp = Date.now() - 1000; await app.activityCache.write("sr", "all", stamp, []); const oldKey = Object.keys(values).find(key => key.includes("activities.v1:")); const oldEntry = values[oldKey]; await app.activityCache.invalidate(); assert.equal(values[oldKey], undefined); assert.equal(values.preferences, "keep"); values[oldKey] = oldEntry; assert.equal(await app.activityCache.read("sr", "all", stamp), null); delete values[oldKey]; await app.activityCache.write("sr", "all", stamp, []); assert.equal(values[oldKey], undefined); }); function content() { const app = loadApplication("content", { document: { createElement: () => ({ children: [], append(node) { this.children.push(node); }, listeners: {}, attributes: {}, addEventListener(key, fn) { this.listeners[key] = fn; }, setAttribute(key, value) { this.attributes[key] = value; } }) } }); app.contentIcons.createRefreshIcon = () => ({ tag: "svg" }); for (const [object, method] of [ ["opportunityCrmServiceRequests", "reset"], ["opportunityWorkloadActions", "reset"], ["consumptionPlansView", "closeAccountPlansModal"], ["installBaseView", "closeInstallBaseModal"], ["serviceRequestsView", "closeServiceRequestsModal"], ["productsContentJobs", "renderProductsProgress"], ["opportunitiesTable", "setOpportunitiesTableState"] ]) app[object][method] = () => {}; return app; } test("Refresh all deduplicates clicks, clears memory before reload and preserves identity/preferences", async () => { const app = content(); const calls = []; let finish; app.activityCache.invalidate = () => { calls.push("activities"); return new Promise(resolve => { finish = resolve; }); }; app.platformContentMessages.sendRuntimeMessage = async message => { calls.push(message.type); return { ok: true }; }; app.opportunitiesController.requestOpportunitiesForPeriod = async period => calls.push(period); app.authSessionState.accessToken = "synthetic"; app.preferencesState.opptyProductsCacheHours = 48; app.productsContentState.opptyProductsByNumber.set("A", {}); app.consumptionTrendState.consumptionTrendCache.set("A", {}); const first = app.refreshAll.refresh("Next Quarter"); assert.equal(app.refreshAll.refresh("Next Quarter"), first); assert.equal(app.productsContentState.opptyProductsByNumber.size, 0); assert.equal(app.consumptionTrendState.consumptionTrendCache.size, 0); assert.deepEqual(calls, ["activities"]); finish(); await first; assert.deepEqual(calls, ["activities", "opportunitiesExtension.invalidateProductsCache", "Next Quarter"]); assert.equal(app.authSessionState.accessToken, "synthetic"); assert.equal(app.preferencesState.opptyProductsCacheHours, 48); }); test("Refresh all reports failures, does not reload on failed invalidation, and re-enables the button", async () => { const app = content(); app.activityCache.invalidate = async () => { throw new Error("Synthetic storage failure"); }; let notified = ""; app.opportunitiesLinks.showCopyConfirmation = message => { notified = message; }; app.opportunitiesController.requestOpportunitiesForPeriod = () => assert.fail("must not reload"); const button = app.refreshAll.createButton(() => "Current Quarter"); const request = button.listeners.click(); assert.equal(button.disabled, true); await request; assert.equal(button.disabled, false); assert.equal(button.title, "Refresh all"); assert.equal(button.attributes["aria-label"], "Refresh all"); assert.equal(button.children.length, 1); assert.equal(button.children[0].tag, "svg"); assert.equal(button.attributes["aria-busy"], "false"); assert.equal(notified, "Synthetic storage failure"); });