105 lines
5.7 KiB
JavaScript
105 lines
5.7 KiB
JavaScript
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");
|
|
});
|