Added the Refresh All button to invalidate caches and force requests to be sent again

This commit is contained in:
2026-09-25 15:26:09 -03:00
parent 6a3e9e6adf
commit cc567c8ff1
31 changed files with 1211 additions and 143 deletions

104
tests/refreshAll.test.cjs Normal file
View File

@@ -0,0 +1,104 @@
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");
});