General source code refactoring
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
const { loadApplication } = require("./support/application.cjs");
|
||||
|
||||
class Element {
|
||||
constructor(tag) {
|
||||
@@ -28,24 +26,6 @@ function setup(mockSalesToken = true) {
|
||||
static now() { return now; }
|
||||
}
|
||||
let visibleAlerts = [];
|
||||
const source = fs.readFileSync(path.join(__dirname, "../src/content.js"), "utf8");
|
||||
const marker = " if (!isAllowedPage()) {";
|
||||
assert.ok(source.includes(marker));
|
||||
const instrumented = source.replace(marker, `
|
||||
accessToken = "test-bearer-secret";
|
||||
getXsrfToken = async () => ({ token: "test-xsrf-cookie", cookieName: "XSRF-TOKEN-DYNAMIC", source: "test" });
|
||||
refreshXsrfCookie = async () => ({ refreshed: false });
|
||||
${mockSalesToken ? 'requestTokenRelaySalesCommon = async () => "test-sales-common-secret";' : ''}
|
||||
renderDebugPanel = () => {};
|
||||
renderOpportunitiesTable = () => {};
|
||||
globalThis.helpers = {
|
||||
requestGenerateAiSummary, createProductsCountButton, createProductsDetailRow,
|
||||
createAiSummaryAlert, aiSummariesByOptyNumber, expandedProductOptyNumbers,
|
||||
opptyProductsByNumber, requestLog, formatHeaderBlock, requestTokenRelay,
|
||||
getAccessToken: () => accessToken, requestTokenRelaySalesCommon
|
||||
};
|
||||
return;
|
||||
${marker}`);
|
||||
const context = {
|
||||
document: {
|
||||
createElement: (tag) => new Element(tag),
|
||||
@@ -54,8 +34,21 @@ ${marker}`);
|
||||
fetch: (url, options) => new Promise((resolve, reject) => calls.push({ url, options, resolve, reject })),
|
||||
Intl, Date: TestDate, Map, Set
|
||||
};
|
||||
vm.runInNewContext(instrumented, context);
|
||||
return { ...context.helpers, calls, advance: (ms) => { now += ms; }, show: (alerts) => { visibleAlerts = alerts; } };
|
||||
const app = loadApplication("content", context);
|
||||
app.authSessionState.accessToken = "test-bearer-secret";
|
||||
app.authXsrfContent.getXsrfToken = async () => ({ token: "test-xsrf-cookie", cookieName: "XSRF-TOKEN-DYNAMIC", source: "test" });
|
||||
app.platformContentMessages.refreshXsrfCookie = async () => ({ refreshed: false });
|
||||
if (mockSalesToken) app.authSalesCommon.requestTokenRelaySalesCommon = async () => "test-sales-common-secret";
|
||||
app.debugContentView.renderDebugPanel = () => {};
|
||||
app.opportunitiesTable.renderOpportunitiesTable = () => {};
|
||||
return {
|
||||
...app.aiSummaryContent, ...app.productsDetails, ...app.aiSummaryState,
|
||||
...app.productsContentState, ...app.debugContentLog, ...app.debugState,
|
||||
requestTokenRelay: app.authTokenRelay.requestTokenRelay,
|
||||
requestTokenRelaySalesCommon: app.authSalesCommon.requestTokenRelaySalesCommon,
|
||||
getAccessToken: () => app.authSessionState.accessToken,
|
||||
calls, advance: (ms) => { now += ms; }, show: (alerts) => { visibleAlerts = alerts; }
|
||||
};
|
||||
}
|
||||
|
||||
function json(data, status = 200) {
|
||||
|
||||
75
tests/architecture.test.cjs
Normal file
75
tests/architecture.test.cjs
Normal file
@@ -0,0 +1,75 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
const { createHash } = require("node:crypto");
|
||||
const { test } = require("node:test");
|
||||
const { sources, composeScript } = require("../scripts/compose.cjs");
|
||||
const { loadApplication } = require("./support/application.cjs");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const read = file => fs.readFileSync(path.join(root, file), "utf8");
|
||||
const json = value => JSON.parse(JSON.stringify(value));
|
||||
|
||||
test("every source belongs to an explicit runtime composition, with no duplicate factories", () => {
|
||||
const actual = fs.readdirSync(path.join(root, "src"), { recursive: true })
|
||||
.filter(file => file.endsWith(".js")).map(file => `src/${file.replaceAll(path.sep, "/")}`).sort();
|
||||
assert.deepEqual([...new Set(Object.values(sources).flat())].sort(), actual);
|
||||
for (const [target, files] of Object.entries(sources)) {
|
||||
assert.equal(new Set(files).size, files.length);
|
||||
const factories = files.flatMap(file => [...read(file).matchAll(/^function (\w+)\(/gm)].map(match => match[1]));
|
||||
assert.equal(new Set(factories).size, factories.length, target);
|
||||
assert.equal(composeScript(target), composeScript(target));
|
||||
assert.doesNotThrow(() => new vm.Script(composeScript(target)));
|
||||
for (const file of files) {
|
||||
const source = read(file);
|
||||
assert.doesNotMatch(source, /\beval\s*\(|\bnew\s+Function\s*\(|\bimportScripts\s*\(|\brequire\s*\(|^\s*(?:import|export)\s/m, file);
|
||||
if (file.endsWith("/application.js") || file.endsWith("/main.js")) continue;
|
||||
const declaration = source.match(/^function (\w+)\(/m)[1];
|
||||
for (const factory of factories.filter(name => name !== declaration)) {
|
||||
assert.doesNotMatch(source, new RegExp(`\\b${factory}\\s*\\(`), `${file} must receive dependencies, not construct ${factory}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(sources.content.filter(file => sources.background.includes(file)), ["src/shared/messages.js"]);
|
||||
});
|
||||
|
||||
test("Redwood cascade is byte-identical to the characterized original", () => {
|
||||
const hash = createHash("sha256").update(read("src/content/styles/redwood.css")).digest("hex");
|
||||
assert.equal(hash, "eaeb7e53757d48a3095f9111c5029d2c923d0dcab92af23454ccd14bdcfc81c4");
|
||||
});
|
||||
|
||||
test("fiscal module can run alone without DOM, browser APIs or network", () => {
|
||||
const factory = new vm.Script(`${read("src/features/opportunities/fiscal-periods.js")}\ncreateOpportunitiesFiscalPeriods;`).runInNewContext();
|
||||
assert.deepEqual(json(factory().getFiscalPeriodRange("Current Fiscal Year", new Date(2027, 0, 15))), {
|
||||
startCloseDate: "2026-06-01T00:00:00.000Z", endCloseDate: "2027-05-31T00:00:00.000Z"
|
||||
});
|
||||
});
|
||||
|
||||
test("per-frame state and credentials do not leak between application instances", () => {
|
||||
const a = loadApplication("content");
|
||||
const b = loadApplication("content");
|
||||
for (const key of Object.keys(a).filter(key => key.endsWith("State"))) assert.notEqual(a[key], b[key], key);
|
||||
a.authSessionState.accessToken = "synthetic-general";
|
||||
a.authSalesCommonState.salesCommonToken = { access_token: "synthetic-scoped", expiresAt: 123 };
|
||||
a.productsContentState.opptyProductsByNumber.set("TEST", { items: [] });
|
||||
a.aiSummaryState.aiSummariesByOptyNumber.set("TEST", { summaryText: "Synthetic" });
|
||||
assert.equal(b.authSessionState.accessToken, "");
|
||||
assert.equal(b.authSalesCommonState.salesCommonToken, null);
|
||||
assert.equal(b.productsContentState.opptyProductsByNumber.size, 0);
|
||||
assert.equal(b.aiSummaryState.aiSummariesByOptyNumber.size, 0);
|
||||
});
|
||||
|
||||
test("blocked localStorage access remains lazy and handled by preference fallbacks", () => {
|
||||
const context = {};
|
||||
Object.defineProperty(context, "localStorage", { get() { throw new Error("Storage blocked"); } });
|
||||
const app = loadApplication("content", context);
|
||||
assert.doesNotThrow(() => app.preferencesStorage.getSavedOpportunityTypeValues());
|
||||
assert.equal(app.preferencesStorage.getSavedOpptyProductsCacheHours(), 24);
|
||||
});
|
||||
|
||||
test("visual fixtures load modules without eval or rewriting source markers", () => {
|
||||
for (const file of ["tests/fixtures/consumption-trend.html", "tests/fixtures/extension.js"]) {
|
||||
assert.doesNotMatch(read(file), /\beval\s*\(|testableSource|src\/content\.js/);
|
||||
}
|
||||
});
|
||||
344
tests/characterization.test.cjs
Normal file
344
tests/characterization.test.cjs
Normal file
@@ -0,0 +1,344 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
const { root, plain, response, loadContent, loadBackground } = require("./support/characterization.cjs");
|
||||
const { composeScript } = require("./support/application.cjs");
|
||||
|
||||
const PREFIX = "opportunitiesExtension.";
|
||||
const FILTER_KEY = "opportunities-extension-filter-preferences";
|
||||
const TYPE_KEY = "opportunities-extension-opportunity-type-preferences";
|
||||
const CACHE_KEY = "opportunitiesExtension.opptyProductsCache.v1";
|
||||
const DAY = 86400000;
|
||||
const range = { startCloseDate: "2026-06-01T00:00:00.000Z", endCloseDate: "2026-08-31T00:00:00.000Z" };
|
||||
const drain = () => new Promise(resolve => setImmediate(resolve));
|
||||
const productRequest = (requestId, optyNumbers = ["SYNTH-1"]) => ({
|
||||
type: PREFIX + "requestOpptyProducts", requestId, optyNumbers, accessToken: "synthetic-products-token"
|
||||
});
|
||||
|
||||
test("content bootstrap ignores other origins and preserves the existing tile/listener on reinjection", () => {
|
||||
const source = composeScript("content");
|
||||
const script = new vm.Script(source);
|
||||
assert.doesNotThrow(() => script.runInNewContext({
|
||||
window: { location: { hostname: "unrelated.example", pathname: "/" } },
|
||||
document: { getElementById() { assert.fail("DOM touched on unrelated origin"); } }
|
||||
}));
|
||||
let listeners = 0;
|
||||
const context = vm.createContext({
|
||||
window: { location: { hostname: "eeho.fa.us2.oraclecloud.com", pathname: "/fscmUI/faces/FuseWelcome" } },
|
||||
document: { getElementById: () => ({}) },
|
||||
chrome: { runtime: { onMessage: { addListener() { listeners++; } } } },
|
||||
MutationObserver: class { constructor() { assert.fail("Observer unnecessary when tile already exists"); } }
|
||||
});
|
||||
script.runInContext(context);
|
||||
script.runInContext(context);
|
||||
assert.equal(listeners, 1);
|
||||
});
|
||||
|
||||
test("distribution contracts: manifests, execution boundaries and deterministic composition", () => {
|
||||
const base = {
|
||||
manifest_version: 3, name: "Opportunities Extension",
|
||||
description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
||||
version: "1.0.1", permissions: ["cookies", "storage"],
|
||||
host_permissions: ["https://eeho.fa.us2.oraclecloud.com/*", "https://spa.oracle.com/*"],
|
||||
content_scripts: [{
|
||||
matches: ["https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*", "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome*"],
|
||||
js: ["content.js"], all_frames: true, run_at: "document_idle"
|
||||
}]
|
||||
};
|
||||
for (const target of ["chromium", "firefox"]) {
|
||||
const expected = target === "chromium"
|
||||
? { ...base, background: { service_worker: "background.js" } }
|
||||
: { ...base, background: { scripts: ["background.js"] }, browser_specific_settings: { gecko: { id: "opportunities-extension@local.dev" } } };
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(root, "dist", target, "manifest.json"))), expected);
|
||||
for (const file of ["content.js", "background.js"]) {
|
||||
const output = fs.readFileSync(path.join(root, "dist", target, file), "utf8");
|
||||
assert.equal(output, composeScript(file.replace(".js", "")));
|
||||
assert.doesNotThrow(() => new vm.Script(output, { filename: `${target}/${file}` }));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("fiscal dates preserve quarter/year boundaries, leap year and unsupported renewals", () => {
|
||||
const h = loadContent();
|
||||
const cases = [
|
||||
["Current Quarter", [2026, 5, 1], "2026-06-01", "2026-08-31"],
|
||||
["Current Quarter", [2026, 7, 31], "2026-06-01", "2026-08-31"],
|
||||
["Current Quarter", [2026, 8, 1], "2026-09-01", "2026-11-30"],
|
||||
["Current Quarter", [2028, 1, 29], "2027-12-01", "2028-02-29"],
|
||||
["Next Quarter", [2026, 10, 30], "2026-12-01", "2027-02-28"],
|
||||
["Previous Quarter", [2026, 7, 24], "2026-03-01", "2026-05-31"],
|
||||
["Current Fiscal Year", [2026, 4, 31], "2025-06-01", "2026-05-31"],
|
||||
["Current Fiscal Year", [2026, 5, 1], "2026-06-01", "2027-05-31"],
|
||||
["Current Fiscal Year", [2027, 0, 1], "2026-06-01", "2027-05-31"],
|
||||
["4 Rolling Quarters (CQ + 3)", [2026, 8, 16], "2026-09-01", "2027-08-31"]
|
||||
];
|
||||
for (const [period, date, start, end] of cases) {
|
||||
assert.deepEqual(plain(h.getFiscalPeriodRange(period, new Date(...date))), {
|
||||
startCloseDate: start + "T00:00:00.000Z", endCloseDate: end + "T00:00:00.000Z"
|
||||
});
|
||||
}
|
||||
assert.equal(h.getFiscalPeriodRange("RENEWALS (Current + Past Due)", new Date()), null);
|
||||
});
|
||||
|
||||
test("opportunity query preserves fields, record-set OR, date AND, and pagination payload", () => {
|
||||
const h = loadContent();
|
||||
const types = ["ORA_MYSALESTEAMOPTIES", "ORA_MYASSGTERROPTIES", "ORA_CREDITRECEIVER_ISME"];
|
||||
h.setTypes(types);
|
||||
const fields = ["Name", "OptyNumber", "PrimaryRevenue.WinProb", "CustomerAccount", "PrimaryRevenue.RevnAmount", "EffectiveDate", "StatusCode", "DealRisk_c", "Owner", "ForecastGroup_c", "LastUpdateDate", "PrimaryRevenue.RevnAmountCurcyCode", "OptyId"];
|
||||
assert.deepEqual(plain(h.createOpportunitiesQueryPayload(range, 15)), {
|
||||
aggregationResults: false, applications: ["ORACLE-ISS-APP"], onlyData: true, entity: "Opportunity",
|
||||
q: { op: "$and", criteria: [
|
||||
{ op: "$or", criteria: types.map(value => ({ op: "$eq", attribute: "RecordSet", value })) },
|
||||
{ op: "$wi", attribute: "EffectiveDate", value1: range.startCloseDate, value2: range.endCloseDate, dynamicDate: false }
|
||||
] }, keywords: null, keywordsFields: fields,
|
||||
fields: [...fields, "CustomerAccount.PartyUniqueName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber"],
|
||||
sort: [], language: "en", skipInValidFields: true, skipHiddenFromUIFields: true,
|
||||
copiedFrom: "queries/d5dbcd01-9ff3-40be-9b5b-64744cbf7162", limit: 15, offset: 15
|
||||
});
|
||||
});
|
||||
|
||||
test("opportunity pagination uses response offsets/count, ignores stale versions and detects stalled pages", async () => {
|
||||
const calls = [];
|
||||
const h = loadContent({ fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
return response(calls.length === 1
|
||||
? { items: [{ OptyNumber: "S1" }], offset: 0, count: 1, hasMore: true }
|
||||
: { items: [{ OptyNumber: "S2" }], offset: 1, count: 1, hasMore: false });
|
||||
} });
|
||||
const items = await h.requestOpportunitiesPage("synthetic-token", "Current Quarter", range, 0, [], 0, 1);
|
||||
assert.deepEqual(plain(items), [{ OptyNumber: "S1" }, { OptyNumber: "S2" }]);
|
||||
assert.deepEqual(calls.map(call => JSON.parse(call.options.body).offset), [0, 1]);
|
||||
assert.equal(calls[0].url, "https://eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/11.13.18.05/custom-actions/queries");
|
||||
assert.deepEqual(plain(calls[0].options.headers), {
|
||||
Accept: "application/json", Authorization: "Bearer synthetic-token", "Content-Type": "application/json",
|
||||
Origin: "https://eeho.fa.us2.oraclecloud.com", Preference: "transient"
|
||||
});
|
||||
assert.equal(calls[0].options.method, "POST");
|
||||
assert.equal(calls[0].options.credentials, "include");
|
||||
h.setVersion(1);
|
||||
assert.equal(await h.requestOpportunitiesPage("unused", "Current Quarter", range, 0, [], 0, 1), null);
|
||||
assert.equal(calls.length, 2);
|
||||
const stalled = loadContent({ fetch: async () => response({ items: [], count: 0, hasMore: true }) });
|
||||
await assert.rejects(stalled.requestOpportunitiesPage("synthetic", "Current Quarter", range, 0, [], 0, 1), /without additional results/);
|
||||
});
|
||||
|
||||
const opportunity = (number, status, amount) => ({
|
||||
Name: `Synthetic cloud ${number}`, OptyNumber: number, StatusCode: status, ForecastGroup_c: "PIPELINE",
|
||||
CustomerAccount: { PartyId: "customer-1", PartyUniqueName: "Synthetic Customer", PartyNumber: "party-1" },
|
||||
Owner: { PartyId: "owner-1", PersonFirstName: "Test", PersonLastName: "Owner", PartyName: "Long Test Owner Name" },
|
||||
PrimaryRevenue: { RevnAmount: amount, RevnAmountCurcyCode: "USD", WinProb: 60 },
|
||||
EffectiveDate: "2026-08-28", LastUpdateDate: "2026-08-28T13:40:00"
|
||||
});
|
||||
|
||||
test("filter storage round-trip, WON_PENDING membership, search, sorting and reference resets", () => {
|
||||
const preferences = { period: "Next Quarter", stages: ["PIPELINE"], statuses: ["WON"],
|
||||
customerKeys: ["party:customer-1"], ownerKeys: ["party:owner-1"],
|
||||
customerSearch: "Synthetic", ownerSearch: "Test", tableSearch: "cloud" };
|
||||
const storage = new Map([[FILTER_KEY, JSON.stringify(preferences)]]);
|
||||
const h = loadContent({ storage });
|
||||
const select = { value: "Current Quarter" };
|
||||
h.restoreFilterPreferences(select);
|
||||
assert.equal(select.value, "Next Quarter");
|
||||
const items = [opportunity("S1", "WON", "100"), opportunity("S2", "WON_PENDING", "20"), opportunity("S3", "OPEN", "5")];
|
||||
h.setTable(items, "revenue");
|
||||
assert.deepEqual(plain(h.getSortedOpportunities()).map(item => item.OptyNumber), ["S2", "S1"]);
|
||||
assert.equal(h.getSortedOpportunities()[0].StatusCode, "WON_PENDING");
|
||||
assert.equal(h.getOwnerName(items[0]), "Test Owner");
|
||||
assert.equal(h.matchesTableSearch(items[0], "28/08/2026"), true);
|
||||
assert.equal(h.matchesTableSearch(items[0], "test owner"), true);
|
||||
h.saveFilterPreferences(select.value);
|
||||
assert.deepEqual(JSON.parse(storage.get(FILTER_KEY)), preferences);
|
||||
const reopened = loadContent({ storage });
|
||||
reopened.restoreFilterPreferences({ value: "Current Quarter" });
|
||||
assert.equal(reopened.filters().tableSearch, "cloud");
|
||||
reopened.resetCustomerAndOwnerFiltersForPeriod();
|
||||
const nextItems = [...items, { ...opportunity("S4", "WON", "30"), CustomerAccount: { PartyId: "customer-2", PartyUniqueName: "Another Customer" }, Owner: { PartyId: "owner-2", PartyName: "Another Owner" } }];
|
||||
reopened.setTable(nextItems);
|
||||
assert.deepEqual(plain(reopened.filters().customerKeys).sort(), ["party:customer-1", "party:customer-2"]);
|
||||
assert.deepEqual(plain(reopened.filters().ownerKeys).sort(), ["party:owner-1", "party:owner-2"]);
|
||||
assert.equal(reopened.filters().tableSearch, "cloud");
|
||||
assert.equal(reopened.filters().customerSearch, "");
|
||||
assert.equal(reopened.filters().ownerSearch, "");
|
||||
});
|
||||
|
||||
test("malformed preferences fall back, while theme/type/cache keys retain their schema", () => {
|
||||
const storage = new Map([[FILTER_KEY, "not JSON"], ["opportunities-extension-theme", "dark"],
|
||||
[TYPE_KEY, JSON.stringify({ opportunityTypeValues: ["ORA_MYOPTIES", "UNKNOWN"], opptyProductsCacheHours: 24 })]]);
|
||||
const h = loadContent({ storage });
|
||||
assert.doesNotThrow(() => h.restoreFilterPreferences({ value: "Current Quarter" }));
|
||||
assert.equal(h.getSavedTheme(), "dark");
|
||||
assert.deepEqual(plain(h.getSavedOpportunityTypeValues()), ["ORA_MYOPTIES"]);
|
||||
assert.equal(h.getSavedOpptyProductsCacheHours(), 24);
|
||||
assert.equal(h.filters().tableSearch, "");
|
||||
});
|
||||
|
||||
test("tokenrelay missing-cookie recovery and HTTP failure do not change the request contract", async () => {
|
||||
let reads = 0;
|
||||
const calls = [];
|
||||
const h = loadContent({ xsrf: async () => ({ token: ++reads === 1 ? "" : "synthetic-xsrf", cookieName: "XSRF-TOKEN-SYNTHETIC" }),
|
||||
fetch: async (url, options) => { calls.push({ url, options }); return response({ access_token: "synthetic-access" }); } });
|
||||
assert.equal(await h.requestTokenRelay(), "synthetic-access");
|
||||
assert.equal(h.refreshes.length, 1);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].options.headers["x-xsrf-token"], "synthetic-xsrf");
|
||||
assert.equal(calls[0].options.method, "GET");
|
||||
for (const status of [400, 401, 403]) {
|
||||
let attempts = 0;
|
||||
const rejected = loadContent({ xsrf: async () => ({ token: "synthetic-xsrf" }),
|
||||
fetch: async () => { attempts++; return response({}, status); } });
|
||||
await assert.rejects(rejected.requestTokenRelay(), /failed with status/);
|
||||
assert.equal(attempts, 2);
|
||||
assert.equal(rejected.refreshes.length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
for (const family of ["chrome", "firefox"]) {
|
||||
test(`${family}: products paginate, route progress to sender frame and reuse persisted cache after restart`, async () => {
|
||||
const calls = [];
|
||||
const storage = {};
|
||||
const h = loadBackground({ family, storage, fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
const offset = Number(new URL(url).searchParams.get("offset"));
|
||||
return response({ items: [{ RevnNumber: `SYNTH-${offset}` }], count: 1, offset, hasMore: offset === 0 });
|
||||
} });
|
||||
const result = await h.dispatch(productRequest("job-1", ["SYNTH-1", "SYNTH-1"]));
|
||||
assert.deepEqual(result, { ok: true, requestId: "job-1", completed: 1, total: 1, failed: 0, cacheHits: 0 });
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(new URL(calls[0].url).pathname, "/crmRestApi/rest/rv:be91c002-2e5d-4ed2-a37e-e0c837bf141f/en/11.13.18.05:9/opportunities/SYNTH-1/child/ChildRevenue");
|
||||
assert.equal(new URL(calls[0].url).searchParams.get("q"), "(SplitTypeCode!='DETAILCHILDSPLIT') AND (RecurTypeCode!='CHILDRECUR')");
|
||||
assert.equal(new URL(calls[0].url).searchParams.get("limit"), "15");
|
||||
assert.equal(new URL(calls[1].url).searchParams.get("offset"), "1");
|
||||
assert.equal(calls[0].options.headers.Authorization, "Bearer synthetic-products-token");
|
||||
assert.ok(h.progress.every(event => event.tabId === 7 && event.options.frameId === 2));
|
||||
assert.equal(h.progress[0].message.type, PREFIX + "opptyProductsProgress");
|
||||
assert.equal(h.progress[0].message.items.length, 2);
|
||||
assert.equal(h.progress.at(-1).message.state, "complete");
|
||||
assert.deepEqual(Object.keys(storage[CACHE_KEY]["SYNTH-1"]).sort(), ["fetchedAt", "items"]);
|
||||
const restarted = loadBackground({ family, storage });
|
||||
assert.equal((await restarted.dispatch(productRequest("job-2"))).cacheHits, 1);
|
||||
h.advance(DAY);
|
||||
await h.dispatch(productRequest("job-3"));
|
||||
assert.equal(calls.length, 4, "default TTL expires at 24 hours");
|
||||
});
|
||||
|
||||
test(`${family}: product queue has four workers, shares in-flight results and reports partial failures`, async () => {
|
||||
const calls = [];
|
||||
const h = loadBackground({ family, fetch: (url) => new Promise(resolve => calls.push({ url, resolve })) });
|
||||
const first = h.dispatch(productRequest("concurrent-1", ["S1", "S2", "S3", "S4", "S5"]));
|
||||
await drain();
|
||||
assert.equal(calls.length, 4);
|
||||
const duplicate = h.dispatch(productRequest("concurrent-2", ["S1"]));
|
||||
await drain();
|
||||
assert.equal(calls.length, 4);
|
||||
calls[0].resolve(response({ items: [{ RevnNumber: "R1" }], hasMore: false }));
|
||||
await drain();
|
||||
assert.equal(calls.length, 5);
|
||||
calls[1].resolve(response({}, 500));
|
||||
for (const call of calls.slice(2)) call.resolve(response({ items: [], hasMore: false }));
|
||||
assert.equal((await duplicate).ok, true);
|
||||
const result = await first;
|
||||
assert.equal(result.failed, 1);
|
||||
assert.equal(result.completed, 5);
|
||||
assert.equal(h.progress.filter(event => event.message.requestId === "concurrent-1").at(-1).message.state, "complete-with-errors");
|
||||
});
|
||||
|
||||
test(`${family}: custom product TTL and zero-count pagination failures`, async () => {
|
||||
let calls = 0;
|
||||
const h = loadBackground({ family, fetch: async () => { calls++; return response({ items: [], hasMore: false }); } });
|
||||
const message = { ...productRequest("custom-ttl"), cacheMaxAgeMs: 1000 };
|
||||
await h.dispatch(message);
|
||||
h.advance(999);
|
||||
assert.equal((await h.dispatch(message)).cacheHits, 1);
|
||||
assert.equal(calls, 1);
|
||||
h.advance(1);
|
||||
assert.equal((await h.dispatch(message)).cacheHits, 0);
|
||||
assert.equal(calls, 2);
|
||||
const stalled = loadBackground({ family, fetch: async () => response({ items: [], count: 0, hasMore: true }) });
|
||||
assert.equal((await stalled.dispatch(productRequest("stalled"))).failed, 1);
|
||||
assert.match(stalled.progress[0].message.error, /without additional results/);
|
||||
});
|
||||
|
||||
test(`${family}: dynamic XSRF cookie lookup, missing-cookie result and temporary-tab cleanup`, async () => {
|
||||
const cookie = { name: "XSRF-TOKEN-SYNTHETIC", value: "synthetic-xsrf", domain: "eeho.fa.us2.oraclecloud.com", path: "/", storeId: "test-store" };
|
||||
const h = loadBackground({ family, cookies: () => [cookie] });
|
||||
const result = await h.dispatch({ type: PREFIX + "getXsrfToken" });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.cookieName, cookie.name);
|
||||
assert.equal(result.token, cookie.value);
|
||||
assert.ok(h.cookieQueries.some(query => query.storeId === "test-store"));
|
||||
const missing = loadBackground({ family });
|
||||
assert.equal((await missing.dispatch({ type: PREFIX + "getXsrfToken" })).token, "");
|
||||
const refresh = await missing.dispatch({ type: PREFIX + "refreshXsrfCookie" });
|
||||
assert.equal(refresh.refreshed, false);
|
||||
assert.equal(refresh.durationMs, 15000);
|
||||
assert.deepEqual(missing.removedTabs, [91]);
|
||||
assert.equal(missing.tabs[0].active, false);
|
||||
assert.equal(missing.tabs[0].windowId, 3);
|
||||
assert.equal(missing.tabs[0].cookieStoreId, family === "firefox" ? "test-store" : undefined);
|
||||
let refreshHarness;
|
||||
refreshHarness = loadBackground({ family, cookies: () => refreshHarness.tabs.length ? [cookie] : [] });
|
||||
assert.equal((await refreshHarness.dispatch({ type: PREFIX + "refreshXsrfCookie" })).refreshed, true);
|
||||
assert.deepEqual(refreshHarness.removedTabs, [91]);
|
||||
});
|
||||
|
||||
test(`${family}: SPA token selection, account POST contracts and debug envelope`, async () => {
|
||||
const calls = [];
|
||||
const h = loadBackground({ family, fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
if (calls.length === 1) return response([
|
||||
{ tokenService: "UNRELATED", uiaasHeader: { value: "synthetic-unused" } },
|
||||
{ tokenService: "ACCP-TS", uiaasHeader: { value: "synthetic-account" } },
|
||||
{ tokenService: "OCICONS-TS", uiaasHeader: { value: "synthetic-consumption" } }
|
||||
]);
|
||||
if (calls.length === 2) return response({ items: [{ id: "synthetic/id" }] });
|
||||
return response({ items: [{ planType: "Synthetic", subPlanNum: "plan-1" }] });
|
||||
} });
|
||||
const tokens = await h.dispatch({ type: PREFIX + "requestConsumerTokenService" });
|
||||
const plans = await h.dispatch({ type: PREFIX + "requestAccountPlans", partyNumber: "synthetic-party", authorizations: tokens.authorizations });
|
||||
assert.equal(plans.ok, true);
|
||||
assert.equal(plans.accountPlanId, "synthetic/id");
|
||||
assert.deepEqual(plans.plans, [{ planType: "Synthetic", subPlanNum: "plan-1" }]);
|
||||
assert.equal(calls[1].url, "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary");
|
||||
assert.deepEqual(JSON.parse(calls[1].options.body), [{ key: "regId", value: "synthetic-party" }]);
|
||||
assert.equal(calls[1].options.headers["account-ti-authorization"], "synthetic-account");
|
||||
assert.equal(calls[2].options.headers["g2m-authorization"], "synthetic-consumption");
|
||||
assert.equal(calls[2].url, "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans/synthetic%2Fid");
|
||||
assert.deepEqual(JSON.parse(calls[2].options.body), {
|
||||
hierarchy: "single", products: "all", language: "english",
|
||||
planStatus: ["active", "awaiting_provisioning", "awaiting_provisioning_error", "draft", "expired", "final_billing", "onhold", "provisioned", "signed", "superseded", "superseded_pending_final_billing", "suspended", "terminated", "terminated_pending_final_billing"]
|
||||
});
|
||||
assert.ok(calls.slice(1).every(call => call.options.method === "POST" && call.options.credentials === "include"));
|
||||
assert.deepEqual(plans.debugRequests.map(entry => entry.name), ["requestAccountSummary", "requestAccountPlans"]);
|
||||
assert.ok(plans.debugRequests.every(entry => entry.httpStatus === 200 && entry.status === "success" && entry.durationMs >= 0));
|
||||
});
|
||||
|
||||
test(`${family}: unknown/invalid messages fail without network; storage failure does not discard fetched products`, async () => {
|
||||
const h = loadBackground({ family });
|
||||
assert.equal(h.listener({ type: "unknown" }, {}, () => assert.fail("Unexpected reply")), false);
|
||||
assert.equal((await h.dispatch({ type: PREFIX + "requestOpptyProducts" })).ok, false);
|
||||
assert.equal((await h.dispatch({ type: PREFIX + "requestAccountPlans" })).ok, false);
|
||||
assert.equal((await h.dispatch({ type: PREFIX + "requestConsumptionTrend", subPlanNum: "S1", period: "unsupported" })).ok, false);
|
||||
const noStorage = loadBackground({ family, storageError: true, fetch: async () => response({ items: [{ RevnNumber: "R1" }], hasMore: false }) });
|
||||
assert.equal((await noStorage.dispatch(productRequest("no-storage"))).ok, true);
|
||||
assert.equal(noStorage.progress[0].message.items.length, 1);
|
||||
});
|
||||
|
||||
test(`${family}: consumption network/HTTP/JSON failures retain the response and debug contracts`, async () => {
|
||||
for (const failure of ["network", "http", "json"]) {
|
||||
const h = loadBackground({ family, fetch: async () => {
|
||||
if (failure === "network") throw new Error("Synthetic network error");
|
||||
return failure === "http" ? response({}, 401) : new Response("<html>Synthetic fallback</html>");
|
||||
} });
|
||||
const result = await h.dispatch({ type: PREFIX + "requestConsumptionTrend", subPlanNum: "synthetic-plan", period: "weekly",
|
||||
authorization: { name: "g2m-authorization", value: "synthetic-token" } });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.debugRequests.length, 1);
|
||||
assert.equal(result.debugRequests[0].name, "requestConsumptionServiceWeekly");
|
||||
assert.equal(result.debugRequests[0].status, "failed");
|
||||
assert.equal(result.debugRequests[0].httpStatus, failure === "network" ? null : failure === "http" ? 401 : 200);
|
||||
assert.match(result.error, failure === "network" ? /Synthetic network error/ : failure === "http" ? /status 401/ : /invalid JSON/);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
const { composeScript } = require("./support/application.cjs");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
|
||||
function createResponse(body, contentType) {
|
||||
return {
|
||||
@@ -24,7 +22,7 @@ function createResponse(body, contentType) {
|
||||
|
||||
function loadBackground(responses, createdTabs, removedTabs, options = {}) {
|
||||
let listener;
|
||||
const source = fs.readFileSync(path.join(root, "src", "background.js"), "utf8");
|
||||
const source = composeScript("background");
|
||||
const context = {
|
||||
chrome: {
|
||||
runtime: {
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
const { loadApplication, composeScript } = require("./support/application.cjs");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
|
||||
function loadContentHelpers() {
|
||||
const source = fs.readFileSync(path.join(root, "src", "content.js"), "utf8");
|
||||
const marker = "\n if (insertTile()) {";
|
||||
const instrumented = source.replace(marker, `
|
||||
globalThis.__consumptionTrendTest = {
|
||||
normalizeConsumptionTrendItems,
|
||||
formatConsumptionPointLabel,
|
||||
createNiceConsumptionMaximum,
|
||||
calculateAverageConsumption
|
||||
};
|
||||
${marker}`);
|
||||
const context = {
|
||||
chrome: { runtime: { onMessage: { addListener() {} } } },
|
||||
document: { getElementById: () => ({}) },
|
||||
@@ -36,13 +24,12 @@ ${marker}`);
|
||||
Math
|
||||
};
|
||||
|
||||
vm.runInNewContext(instrumented, context);
|
||||
return context.__consumptionTrendTest;
|
||||
return loadApplication("content", context).consumptionModel;
|
||||
}
|
||||
|
||||
function loadBackground(fetchCalls) {
|
||||
let listener;
|
||||
const source = fs.readFileSync(path.join(root, "src", "background.js"), "utf8");
|
||||
const source = composeScript("background");
|
||||
const headers = {
|
||||
get(name) {
|
||||
return name.toLowerCase() === "content-type" ? "application/json" : null;
|
||||
|
||||
36
tests/fixtures/consumption-trend.html
vendored
36
tests/fixtures/consumption-trend.html
vendored
@@ -12,25 +12,21 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="opportunities-extension-modal" class="opportunities-extension-modal" data-theme="light"></div>
|
||||
<script src="/fixture-runtime.js"></script>
|
||||
<script>
|
||||
window.chrome = { runtime: { onMessage: { addListener() {} } } };
|
||||
|
||||
async function initialize() {
|
||||
const source = await fetch("/src/content.js").then((response) => response.text());
|
||||
const marker = /\r?\n if \(insertTile\(\)\) \{/;
|
||||
const exposure = `
|
||||
window.__trendHarness = {
|
||||
ensureStyles: ensureExtensionStyles,
|
||||
createAccountPlanDetails,
|
||||
createConsumptionTrendSection,
|
||||
renderConsumptionTrendChart
|
||||
};
|
||||
return;
|
||||
`;
|
||||
const testableSource = source
|
||||
.replace(" if (!isAllowedPage()) {", " if (false) {")
|
||||
.replace(marker, `${exposure}\n if (insertTile()) {`);
|
||||
window.eval(testableSource);
|
||||
const app = window.createFixtureApplication({
|
||||
fetch: () => Promise.reject(new Error("Network disabled in fixture")),
|
||||
browser: undefined,
|
||||
chrome: undefined
|
||||
});
|
||||
const harness = {
|
||||
ensureStyles: app.contentStyles.ensureExtensionStyles,
|
||||
createAccountPlanDetails: app.consumptionPlansView.createAccountPlanDetails,
|
||||
createConsumptionTrendSection: app.consumptionTrendController.createConsumptionTrendSection,
|
||||
renderConsumptionTrendChart: app.consumptionChart.renderConsumptionTrendChart
|
||||
};
|
||||
|
||||
const plan = {
|
||||
subPlanNum: "11276098",
|
||||
@@ -63,7 +59,7 @@
|
||||
return rows;
|
||||
});
|
||||
|
||||
window.__trendHarness.ensureStyles();
|
||||
harness.ensureStyles();
|
||||
const overlay = document.getElementById("opportunities-extension-modal");
|
||||
overlay.dataset.theme = new URLSearchParams(window.location.search).get("theme") === "dark"
|
||||
? "dark"
|
||||
@@ -82,13 +78,13 @@
|
||||
tabs.innerHTML = '<button class="opportunities-extension-account-plan-tab" aria-selected="true"><span class="opportunities-extension-account-plan-status-indicator" data-active="true"></span>Universal Credits 11276098</button>';
|
||||
const panel = document.createElement("section");
|
||||
panel.className = "opportunities-extension-account-plan-panel";
|
||||
const trend = window.__trendHarness.createConsumptionTrendSection(plan);
|
||||
panel.append(window.__trendHarness.createAccountPlanDetails(plan), trend);
|
||||
const trend = harness.createConsumptionTrendSection(plan);
|
||||
panel.append(harness.createAccountPlanDetails(plan), trend);
|
||||
content.append(tabs, panel);
|
||||
dialog.append(header, content);
|
||||
modal.append(dialog);
|
||||
overlay.append(modal);
|
||||
window.__trendHarness.renderConsumptionTrendChart(
|
||||
harness.renderConsumptionTrendChart(
|
||||
trend,
|
||||
trend.querySelector(".opportunities-extension-consumption-chart"),
|
||||
items,
|
||||
|
||||
13
tests/fixtures/extension.html
vendored
Normal file
13
tests/fixtures/extension.html
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Extension Synthetic Integration Fixture</title>
|
||||
</head>
|
||||
<body>
|
||||
<button id="open" type="button">Open Opportunities</button>
|
||||
<script src="/fixture-runtime.js"></script>
|
||||
<script src="/extension.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
77
tests/fixtures/extension.js
vendored
Normal file
77
tests/fixtures/extension.js
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
(() => {
|
||||
const listeners = [];
|
||||
const storage = new Map();
|
||||
const opportunities = Array.from({ length: 25 }, (_, index) => ({
|
||||
OptyNumber: `TEST${index + 1}`,
|
||||
Name: `Synthetic opportunity ${index + 1} - Database migration`,
|
||||
CustomerAccount: { PartyUniqueName: "Example public sector customer (Example City, BR)", PartyId: "test-party", PartyNumber: "test-account" },
|
||||
Owner: { PersonFirstName: "Example", PersonLastName: "Owner" },
|
||||
PrimaryRevenue: { RevnAmountCurcyCode: "USD", RevnAmount: "25000", WinProb: 60 },
|
||||
EffectiveDate: "2026-11-15T00:00:00Z",
|
||||
LastUpdateDate: "2026-09-15T17:00:00Z",
|
||||
ForecastGroup_c: ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"][index % 5],
|
||||
StatusCode: ["OPEN", "CLOSED", "LOST", "WON", "WON_PENDING"][index % 5]
|
||||
}));
|
||||
const products = [{ ProdGroupName: "Migrate databases to cloud", WorkloadName_c: "Example workload", RevnAmountCurcyCode: "USD", RevnAmount: "25000", TypeCode: "NEWCOMMIT", StatusCode: "OPEN", WinProb: 60, EffectiveDate: "2026-11-15", ConsumptionStartDate_c: "2026-11-15", RampMonths_c: 3 }];
|
||||
const trend = Array.from({ length: 30 }, (_, index) => ({
|
||||
fyDate: `2026-09-${String((index % 28) + 1).padStart(2, "0")}`,
|
||||
fiscalTime: [`${String((index % 28) + 1).padStart(2, "0")}-Sep-26`],
|
||||
fiscalQtr: "FY27-Q2", currencyCode: "USD", arrCd: 0,
|
||||
usageCategory: index % 4 ? "Funded Allocation" : "OVERAGE",
|
||||
usedAmount: 1000 + index * 100
|
||||
}));
|
||||
const authorizations = {
|
||||
accountSummary: { label: "Account-Ti-Authorization", value: "synthetic-accp" },
|
||||
accountPlans: { label: "G2m-Authorization", value: "synthetic-ocicons" }
|
||||
};
|
||||
const runtime = {
|
||||
onMessage: { addListener: listener => listeners.push(listener) },
|
||||
async sendMessage(message) {
|
||||
const operation = message.type.split(".").at(-1);
|
||||
if (operation === "getXsrfToken") return { ok: true, token: "synthetic-xsrf", cookieName: "XSRF-TOKEN-TEST" };
|
||||
if (operation === "requestConsumerTokenService") return { ok: true, authorizations, debugRequests: [] };
|
||||
if (operation === "requestOpptyProducts") {
|
||||
message.optyNumbers.forEach((optyNumber, index) => listeners.forEach(listener => listener({
|
||||
type: "opportunitiesExtension.opptyProductsProgress", requestId: message.requestId,
|
||||
optyNumber, items: products, cachedAt: Date.now(), completed: index + 1,
|
||||
total: message.optyNumbers.length, state: index + 1 === message.optyNumbers.length ? "complete" : "running"
|
||||
})));
|
||||
return { ok: true, requestId: message.requestId };
|
||||
}
|
||||
if (operation === "requestAccountPlans") return {
|
||||
ok: true, debugRequests: [], plans: ["ACTIVE", "EXPIRED"].map((hdrStatusCode, index) => ({
|
||||
subPlanNum: `PLAN${index + 1}`, planType: "Universal Credits", hdrStatusCode,
|
||||
bookingCustomer: "Example Customer", soldToPartyName: "Example Sold To",
|
||||
contractId: "synthetic-contract", startDate: "2026-06-01", endDate: "2027-05-31"
|
||||
}))
|
||||
};
|
||||
if (operation === "requestConsumptionTrend") return { ok: true, items: trend, debugRequests: [] };
|
||||
throw new Error(`Unexpected synthetic message: ${operation}`);
|
||||
}
|
||||
};
|
||||
const application = window.createFixtureApplication({
|
||||
browser: { runtime: {
|
||||
onMessage: runtime.onMessage,
|
||||
sendMessage(message, callback) {
|
||||
const result = runtime.sendMessage(message);
|
||||
if (callback) result.then(callback);
|
||||
return result;
|
||||
}
|
||||
} }, chrome: undefined,
|
||||
localStorage: { getItem: key => storage.get(key) ?? null, setItem: (key, value) => storage.set(key, value) },
|
||||
async fetch(input, options) {
|
||||
const url = new URL(input);
|
||||
let body;
|
||||
if (url.pathname === "/fscmRestApi/tokenrelay") body = { access_token: "synthetic-fusion-token", expires_in: 3600 };
|
||||
else if (url.pathname.endsWith("custom-actions/queries")) {
|
||||
const { offset, limit } = JSON.parse(options.body);
|
||||
body = { items: opportunities.slice(offset, offset + limit), totalResults: opportunities.length, count: Math.min(limit, opportunities.length - offset), hasMore: offset + limit < opportunities.length, offset, limit };
|
||||
} else if (url.pathname.endsWith("summarizations/generate")) body = { summaryText: "Synthetic AI summary.\nText is displayed safely: <script>never executed</script>." };
|
||||
else throw new Error(`Unexpected synthetic fetch: ${url.pathname}`);
|
||||
return new Response(JSON.stringify(body), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
});
|
||||
application.productsContentJobs.ensureProductsBackgroundMessageHandler();
|
||||
document.getElementById("open").addEventListener("click", () => application.contentModal.openOpportunitiesModal());
|
||||
application.contentModal.openOpportunitiesModal();
|
||||
})();
|
||||
11
tests/support/application.cjs
Normal file
11
tests/support/application.cjs
Normal file
@@ -0,0 +1,11 @@
|
||||
const vm = require("node:vm");
|
||||
const { composeScript } = require("../../scripts/compose.cjs");
|
||||
|
||||
function loadApplication(target, globals = {}) {
|
||||
const factory = new vm.Script(composeScript(target, { bootstrap: false }), {
|
||||
filename: `${target}.test-bundle.js`
|
||||
}).runInNewContext(globals);
|
||||
return factory();
|
||||
}
|
||||
|
||||
module.exports = { loadApplication, composeScript };
|
||||
134
tests/support/characterization.cjs
Normal file
134
tests/support/characterization.cjs
Normal file
@@ -0,0 +1,134 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
const { loadApplication, composeScript } = require("./application.cjs");
|
||||
|
||||
const root = path.resolve(__dirname, "../..");
|
||||
const plain = (value) => JSON.parse(JSON.stringify(value));
|
||||
const response = (body, status = 200) => new Response(JSON.stringify(body), {
|
||||
status, headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
const unexpectedFetch = async () => { throw new Error("Unexpected network request in isolated test"); };
|
||||
|
||||
function clock() {
|
||||
let now = Date.UTC(2026, 8, 16, 12);
|
||||
class ClockDate extends Date {
|
||||
constructor(...args) { super(...(args.length ? args : [now])); }
|
||||
static now() { return now; }
|
||||
}
|
||||
return { Date: ClockDate, advance: (ms) => { now += ms; } };
|
||||
}
|
||||
|
||||
function loadContent({ fetch = unexpectedFetch, storage = new Map(), xsrf = null } = {}) {
|
||||
const refreshes = [];
|
||||
const context = {
|
||||
fetch, testXsrf: xsrf, refreshes,
|
||||
document: { querySelector: () => ({ value: "Current Quarter" }) },
|
||||
localStorage: {
|
||||
getItem: key => storage.get(key) ?? null,
|
||||
setItem: (key, value) => storage.set(key, value)
|
||||
},
|
||||
Date, Intl, URL
|
||||
};
|
||||
const app = loadApplication("content", context);
|
||||
app.debugContentView.renderDebugPanel = () => {};
|
||||
if (xsrf) app.authXsrfContent.getXsrfToken = xsrf;
|
||||
app.platformContentMessages.refreshXsrfCookie = async () => { refreshes.push(true); return { refreshed: true }; };
|
||||
return {
|
||||
...app.opportunitiesFiscalPeriods, ...app.fusionOpportunities, ...app.authTokenRelay,
|
||||
...app.authSalesCommon, ...app.preferencesStorage, ...app.opportunitiesFilters,
|
||||
...app.opportunitiesFormatters, ...app.contentTheme, ...app.debugState,
|
||||
setVersion: value => { app.opportunitiesQueryState.periodRequestVersion = value; },
|
||||
setTypes: values => { app.preferencesState.selectedOpportunityTypeValues = new Set(values); },
|
||||
setTable: (items, sortKey = "", sortDirection = "ascending") => {
|
||||
app.opportunitiesTableState.opportunitiesTableState = { items, sortKey, sortDirection };
|
||||
app.opportunitiesFilters.resetCustomerFilter(items);
|
||||
app.opportunitiesFilters.resetOwnerFilter(items);
|
||||
},
|
||||
filters: () => {
|
||||
const state = app.opportunitiesFilterState;
|
||||
return {
|
||||
stages: Array.from(state.selectedStages), statuses: Array.from(state.selectedStatuses),
|
||||
customerKeys: Array.from(state.selectedCustomerKeys), ownerKeys: Array.from(state.selectedOwnerKeys),
|
||||
customerSearch: state.customerSearch, ownerSearch: state.ownerSearch, tableSearch: state.tableSearch
|
||||
};
|
||||
},
|
||||
storage, refreshes
|
||||
};
|
||||
}
|
||||
|
||||
function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage = {}, cookies = () => [], storageError = false } = {}) {
|
||||
const time = clock();
|
||||
const listeners = [];
|
||||
const progress = [];
|
||||
const tabs = [];
|
||||
const removedTabs = [];
|
||||
const cookieQueries = [];
|
||||
const api = { runtime: { onMessage: { addListener: fn => listeners.push(fn) } } };
|
||||
const readCookies = details => { cookieQueries.push(plain(details)); return cookies(details); };
|
||||
const getStored = key => ({ [key]: storage[key] === undefined ? undefined : plain(storage[key]) });
|
||||
const store = value => Object.assign(storage, plain(value));
|
||||
const openTab = details => { tabs.push(plain(details)); return { id: 91 }; };
|
||||
const send = (tabId, message, options) => progress.push({ tabId, message: plain(message), options: plain(options || {}) });
|
||||
if (family === "firefox") {
|
||||
api.runtime.getBrowserInfo = async () => ({ name: "Firefox" });
|
||||
api.storage = { local: {
|
||||
get: async key => { if (storageError) throw new Error("Storage unavailable"); return getStored(key); },
|
||||
set: async value => { if (storageError) throw new Error("Storage unavailable"); store(value); }
|
||||
} };
|
||||
api.tabs = { create: async details => openTab(details), remove: async id => removedTabs.push(id),
|
||||
sendMessage: async (tabId, message, options) => send(tabId, message, options) };
|
||||
api.cookies = {
|
||||
get: async details => readCookies(details).find(cookie => cookie.name === details.name) || null,
|
||||
getAll: async details => readCookies(details),
|
||||
getAllCookieStores: async () => [{ id: "test-store" }]
|
||||
};
|
||||
} else {
|
||||
const withStorageError = callback => {
|
||||
api.runtime.lastError = storageError ? { message: "Storage unavailable" } : undefined;
|
||||
try { callback(); } finally { delete api.runtime.lastError; }
|
||||
};
|
||||
api.storage = { local: {
|
||||
get(key, callback) { withStorageError(() => callback(getStored(key))); },
|
||||
set(value, callback) { withStorageError(() => { if (!storageError) store(value); callback(); }); }
|
||||
} };
|
||||
api.tabs = {
|
||||
create(details, callback) { callback(openTab(details)); },
|
||||
remove(id, callback) { removedTabs.push(id); callback(); },
|
||||
sendMessage(tabId, message, options, callback) {
|
||||
if (typeof options === "function") { callback = options; options = undefined; }
|
||||
send(tabId, message, options); callback();
|
||||
}
|
||||
};
|
||||
api.cookies = {
|
||||
get(details, callback) { callback(readCookies(details).find(cookie => cookie.name === details.name) || null); },
|
||||
getAll(details, callback) { callback(readCookies(details)); },
|
||||
getAllCookieStores(callback) { callback([{ id: "test-store" }]); }
|
||||
};
|
||||
}
|
||||
const context = {
|
||||
[family === "firefox" ? "browser" : "chrome"]: api,
|
||||
fetch, URL, Date: time.Date,
|
||||
setTimeout: (callback, ms) => { time.advance(ms); return setTimeout(callback, 0); }
|
||||
};
|
||||
new vm.Script(composeScript("background"), { filename: "background.js" })
|
||||
.runInNewContext(context);
|
||||
assert.equal(listeners.length, 1, "Background must register one listener synchronously");
|
||||
const sender = { id: "synthetic-extension", url: "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome",
|
||||
tab: { id: 7, windowId: 3, cookieStoreId: "test-store" }, frameId: 2 };
|
||||
function dispatch(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let replies = 0;
|
||||
const timer = setTimeout(() => reject(new Error("Message did not receive a response")), 2000);
|
||||
const pending = listeners[0](message, sender, result => {
|
||||
clearTimeout(timer);
|
||||
assert.equal(++replies, 1, "Message must receive exactly one response");
|
||||
resolve(plain(result));
|
||||
});
|
||||
if (pending !== true) { clearTimeout(timer); reject(new Error("Expected asynchronous response contract")); }
|
||||
});
|
||||
}
|
||||
return { dispatch, listener: listeners[0], progress, tabs, removedTabs, cookieQueries, storage, advance: time.advance };
|
||||
}
|
||||
|
||||
module.exports = { root, plain, response, loadContent, loadBackground };
|
||||
Reference in New Issue
Block a user