180 lines
9.6 KiB
JavaScript
180 lines
9.6 KiB
JavaScript
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 setup(family, saved = {}, principal = "user@example.invalid") {
|
|
const local = {
|
|
get: async key => key === null ? { ...saved } : ({ [key]: saved[key] }),
|
|
set: async values => Object.assign(saved, JSON.parse(JSON.stringify(values))),
|
|
remove: async keys => keys.forEach(key => delete saved[key])
|
|
};
|
|
const api = family === "firefox" ? { browser: { storage: { local } } } : {
|
|
chrome: { runtime: {}, storage: { local: {
|
|
get: (key, done) => local.get(key).then(done), set: (values, done) => local.set(values).then(done),
|
|
remove: (keys, done) => local.remove(keys).then(done)
|
|
} } }
|
|
};
|
|
const document = { createElement: () => ({ append() {}, setAttribute() {}, addEventListener() {} }) };
|
|
document.createElementNS = (_, tag) => document.createElement(tag);
|
|
const app = loadApplication("content", { ...api, document });
|
|
app.authSessionState.principal = principal;
|
|
app.currentUser.allowsCrmActivities = () => true;
|
|
app.preferencesState.opptyProductsCacheHours = 24;
|
|
app.opportunitiesQueryState.periodRequestVersion = 1;
|
|
app.activityProgress.update = () => {};
|
|
app.productsContentJobs.queueProductsTableRender = () => {};
|
|
app.debugContentLog.appendBackgroundRequestLog = () => {};
|
|
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic-token" } });
|
|
const calls = [];
|
|
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => {
|
|
calls.push("sr");
|
|
return [{ SrNumber: "SR1", ReportedHours_c: 4, OpportunityName_Id_c: { OptyNumber: "OP1" } }];
|
|
};
|
|
app.platformContentMessages.sendRuntimeMessage = async () => {
|
|
calls.push("actions");
|
|
return { ok: true, actions: [{ owner: principal, name: "Test action" }], hasNext: false };
|
|
};
|
|
async function load(cachedAt, cacheOnly) {
|
|
app.opportunityCrmServiceRequests.reset();
|
|
await app.opportunityCrmServiceRequests.load("synthetic-token", 1, { cachedAt, cacheOnly });
|
|
const products = { items: [{ RevnId: "123" }], cachedAt, fromCache: cacheOnly };
|
|
app.productsContentState.opptyProductsByNumber.set("OP1", products);
|
|
app.opportunityWorkloadActions.loadProducts(products, true, { cachedAt, cacheOnly });
|
|
await flush();
|
|
}
|
|
return { app, calls, load, saved, local };
|
|
}
|
|
|
|
test("all cached opportunities load missing Actions despite filters, then reuse their completed cache", async () => {
|
|
const h = setup("firefox");
|
|
const stamp = Date.now() - 1000;
|
|
const app = h.app;
|
|
app.opportunitiesFilterState.tableSearch = "hide every opportunity";
|
|
app.productsContentJobs.renderProductsProgress = () => {};
|
|
const products = Array.from({ length: 7 }, (_, index) => [String(index + 1), {
|
|
items: [{ RevnId: String(index + 1) }], cachedAt: stamp, fromCache: true
|
|
}]);
|
|
app.productsContentState.opptyProductsByNumber = new Map(products);
|
|
await app.activityCache.write("actions", "1", stamp, []);
|
|
await app.activityCache.write("actions", "2", stamp - 1, []);
|
|
const queried = [];
|
|
app.platformContentMessages.sendRuntimeMessage = async message => {
|
|
queried.push(message.revnId);
|
|
return { ok: true, actions: message.revnId === "3" ? [{ owner: app.authSessionState.principal }] : [], hasNext: false };
|
|
};
|
|
async function cycle(id) {
|
|
app.productsContentState.currentProductsRequestId = id;
|
|
app.productActivities.begin(id, "synthetic-token");
|
|
await app.productActivities.complete(id);
|
|
}
|
|
await cycle("first");
|
|
assert.deepEqual(queried.sort(), ["2", "3", "4", "5", "6", "7"]);
|
|
assert.deepEqual(h.calls, ["sr"], "a missing SR cache must not block the Actions queue");
|
|
for (const [number] of products) {
|
|
assert.equal(app.opportunityWorkloadActions.getSummary(number).state, number === "3" ? "matched" : "missing");
|
|
}
|
|
app.opportunityWorkloadActions.reset();
|
|
app.opportunityCrmServiceRequests.reset();
|
|
await cycle("second");
|
|
assert.equal(queried.length, 6, "valid matching action cache avoids all repeat requests");
|
|
assert.deepEqual(h.calls, ["sr"]);
|
|
});
|
|
for (const family of ["chrome", "firefox"]) {
|
|
test(`${family}: Current Quarter -> Next Quarter -> Current Quarter restores SR hours and filters without requests`, async () => {
|
|
const h = setup(family);
|
|
const currentStamp = Date.now() - 2000;
|
|
const nextStamp = currentStamp + 1000;
|
|
let hours = 2;
|
|
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => {
|
|
h.calls.push("sr");
|
|
return [{ SrNumber: "SR1", ReportedHours_c: hours, OpportunityName_Id_c: { OptyNumber: "OP1" } }];
|
|
};
|
|
await h.load(currentStamp, false);
|
|
hours = 8;
|
|
await h.load(nextStamp, false);
|
|
assert.equal(h.app.opportunityCrmServiceRequests.getSummary("OP1").reportedHours, 8);
|
|
const count = h.calls.length;
|
|
await h.load(currentStamp, true);
|
|
assert.equal(h.calls.length, count);
|
|
assert.equal(h.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "2h");
|
|
h.app.opportunitiesTable.renderOpportunitiesTable = () => {};
|
|
h.app.activityFilters.toggle("sr", "under4");
|
|
assert.equal(h.app.activityFilters.matches({ OptyNumber: "OP1" }), true);
|
|
await h.load(nextStamp, true);
|
|
assert.equal(h.calls.length, count);
|
|
assert.equal(h.app.activityFilters.matches({ OptyNumber: "OP1" }), false);
|
|
const reopened = setup(family, h.saved);
|
|
await reopened.load(currentStamp, true);
|
|
assert.deepEqual(reopened.calls, []);
|
|
assert.equal(reopened.app.opportunityCrmServiceRequests.getSummary("OP1").reportedHours, 2);
|
|
});
|
|
test(`${family}: activities persist across instances with product timestamps and no credential storage`, async () => {
|
|
const h = setup(family);
|
|
const stamp = Date.now() - 1000;
|
|
await h.load(stamp, false);
|
|
assert.deepEqual(h.calls, ["sr", "actions"]);
|
|
assert.equal(Object.keys(h.saved).length, 2);
|
|
Object.values(h.saved).forEach(entry => assert.equal(entry.fetchedAt, stamp));
|
|
assert.doesNotMatch(JSON.stringify(h.saved), /synthetic-token|debugRequests|authorization/i);
|
|
const restored = setup(family, h.saved);
|
|
await restored.load(stamp, true);
|
|
assert.deepEqual(restored.calls, []);
|
|
assert.equal(restored.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "4h");
|
|
assert.equal(restored.app.opportunityWorkloadActions.createBadge("OP1").textContent, "1");
|
|
await restored.load(stamp, false);
|
|
assert.deepEqual(restored.calls, [], "valid matching cache also deduplicates repeated fresh-product notifications");
|
|
const other = setup(family, h.saved, "other@example.invalid");
|
|
await other.load(stamp, true);
|
|
assert.deepEqual(other.calls, []);
|
|
assert.equal(other.app.opportunityCrmServiceRequests.createBadge("OP1"), null);
|
|
assert.equal(other.app.opportunityWorkloadActions.createBadge("OP1").textContent, "?");
|
|
await restored.load(stamp + 1, false);
|
|
assert.deepEqual(restored.calls, ["sr", "actions"]);
|
|
});
|
|
}
|
|
test("TTL follows product preferences, rejects stale/mismatched cache and stores empty success", async () => {
|
|
const h = setup("firefox");
|
|
const stamp = Date.now() - 2 * 3600000;
|
|
await h.app.activityCache.write("sr", "all", stamp, []);
|
|
assert.ok(await h.app.activityCache.read("sr", "all", stamp));
|
|
h.app.preferencesState.opptyProductsCacheHours = 1;
|
|
assert.equal(await h.app.activityCache.read("sr", "all", stamp), null);
|
|
h.app.preferencesState.opptyProductsCacheHours = 24;
|
|
assert.equal(await h.app.activityCache.read("sr", "all", stamp + 1), null);
|
|
assert.equal(await h.app.activityCache.read("sr", "all", Date.now() + 100000), null);
|
|
await h.load(stamp + 1, true);
|
|
assert.deepEqual(h.calls, []);
|
|
});
|
|
|
|
test("versioned SR cache reads matching legacy entries and prunes expired snapshots only", async () => {
|
|
const h = setup("firefox");
|
|
const legacyKey = `opportunitiesExtension.activities.v1:${JSON.stringify(["user@example.invalid", "sr", "all"])}`;
|
|
const stamp = Date.now() - 1000;
|
|
h.saved[legacyKey] = { fetchedAt: stamp, items: [], hasNext: false };
|
|
assert.ok(await h.app.activityCache.read("sr", "all", stamp));
|
|
assert.equal(await h.app.activityCache.read("sr", "all", stamp + 1), null);
|
|
const expiredStamp = Date.now() - 25 * 3600000;
|
|
const expiredKey = `opportunitiesExtension.activities.v1:${JSON.stringify(["user@example.invalid", "sr", "all", expiredStamp])}`;
|
|
h.saved[expiredKey] = { fetchedAt: expiredStamp, items: [], hasNext: false };
|
|
h.saved.preferences = { keep: true };
|
|
await h.app.activityCache.write("sr", "all", stamp + 1, []);
|
|
assert.equal(h.saved[expiredKey], undefined);
|
|
assert.ok(h.saved[legacyKey]);
|
|
assert.equal(h.saved.preferences.keep, true);
|
|
await h.app.activityCache.invalidate();
|
|
assert.equal(await h.app.activityCache.read("sr", "all", stamp + 1), null);
|
|
assert.equal(h.saved.preferences.keep, true);
|
|
});
|
|
test("storage failure does not lose live activity results and failures are never cached", async () => {
|
|
const h = setup("firefox");
|
|
h.local.get = async () => { throw new Error("Synthetic storage read failure"); };
|
|
h.local.set = async () => { throw new Error("Synthetic storage write failure"); };
|
|
await h.load(Date.now() - 1000, false);
|
|
assert.equal(h.app.opportunityCrmServiceRequests.createBadge("OP1").textContent, "4h");
|
|
const failed = setup("firefox");
|
|
failed.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => { throw new Error("Synthetic failure"); };
|
|
failed.app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: false });
|
|
await failed.load(Date.now() - 1000, false);
|
|
assert.deepEqual(failed.saved, {});
|
|
});
|