fune/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
Butkovits Atila 8582db0ea5 Backed out 5 changesets (bug 1866802) for causing failures at test_TopSitesFeed.js. CLOSED TREE
Backed out changeset af9fbbc9ae50 (bug 1866802)
Backed out changeset 81d5b7de7178 (bug 1866802)
Backed out changeset e4d0863ed222 (bug 1866802)
Backed out changeset c0deb681b193 (bug 1866802)
Backed out changeset dda3e5e39f8c (bug 1866802)
2023-12-14 03:22:48 +02:00

2869 lines
94 KiB
JavaScript

import { _ASRouter, MessageLoaderUtils } from "lib/ASRouter.jsm";
import { QueryCache } from "lib/ASRouterTargeting.jsm";
import {
FAKE_LOCAL_MESSAGES,
FAKE_LOCAL_PROVIDER,
FAKE_LOCAL_PROVIDERS,
FAKE_REMOTE_MESSAGES,
FAKE_REMOTE_PROVIDER,
FAKE_REMOTE_SETTINGS_PROVIDER,
} from "./constants";
import {
ASRouterPreferences,
TARGETING_PREFERENCES,
} from "lib/ASRouterPreferences.jsm";
import { ASRouterTriggerListeners } from "lib/ASRouterTriggerListeners.jsm";
import { CFRPageActions } from "lib/CFRPageActions.jsm";
import { GlobalOverrider } from "test/unit/utils";
import { PanelTestProvider } from "lib/PanelTestProvider.sys.mjs";
import ProviderResponseSchema from "content-src/asrouter/schemas/provider-response.schema.json";
const MESSAGE_PROVIDER_PREF_NAME =
"browser.newtabpage.activity-stream.asrouter.providers.cfr";
const FAKE_PROVIDERS = [
FAKE_LOCAL_PROVIDER,
FAKE_REMOTE_PROVIDER,
FAKE_REMOTE_SETTINGS_PROVIDER,
];
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
const FAKE_RESPONSE_HEADERS = { get() {} };
const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
const USE_REMOTE_L10N_PREF =
"browser.newtabpage.activity-stream.asrouter.useRemoteL10n";
// eslint-disable-next-line max-statements
describe("ASRouter", () => {
let Router;
let globals;
let sandbox;
let initParams;
let messageBlockList;
let providerBlockList;
let messageImpressions;
let groupImpressions;
let previousSessionEnd;
let fetchStub;
let clock;
let fakeAttributionCode;
let fakeTargetingContext;
let FakeToolbarBadgeHub;
let FakeToolbarPanelHub;
let FakeMomentsPageHub;
let ASRouterTargeting;
let screenImpressions;
function setMessageProviderPref(value) {
sandbox.stub(ASRouterPreferences, "providers").get(() => value);
}
function initASRouter(router) {
const getStub = sandbox.stub();
getStub.returns(Promise.resolve());
getStub
.withArgs("messageBlockList")
.returns(Promise.resolve(messageBlockList));
getStub
.withArgs("providerBlockList")
.returns(Promise.resolve(providerBlockList));
getStub
.withArgs("messageImpressions")
.returns(Promise.resolve(messageImpressions));
getStub.withArgs("groupImpressions").resolves(groupImpressions);
getStub
.withArgs("previousSessionEnd")
.returns(Promise.resolve(previousSessionEnd));
getStub
.withArgs("screenImpressions")
.returns(Promise.resolve(screenImpressions));
initParams = {
storage: {
get: getStub,
set: sandbox.stub().returns(Promise.resolve()),
},
sendTelemetry: sandbox.stub().resolves(),
clearChildMessages: sandbox.stub().resolves(),
clearChildProviders: sandbox.stub().resolves(),
updateAdminState: sandbox.stub().resolves(),
dispatchCFRAction: sandbox.stub().resolves(),
};
sandbox.stub(router, "loadMessagesFromAllProviders").callThrough();
return router.init(initParams);
}
async function createRouterAndInit(providers = FAKE_PROVIDERS) {
setMessageProviderPref(providers);
// `.freeze` to catch any attempts at modifying the object
Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
await initASRouter(Router);
}
beforeEach(async () => {
globals = new GlobalOverrider();
messageBlockList = [];
providerBlockList = [];
messageImpressions = {};
groupImpressions = {};
previousSessionEnd = 100;
screenImpressions = {};
sandbox = sinon.createSandbox();
ASRouterTargeting = {
isMatch: sandbox.stub(),
findMatchingMessage: sandbox.stub(),
Environment: {
locale: "en-US",
localeLanguageCode: "en",
browserSettings: {
update: {
channel: "default",
enabled: true,
autoDownload: true,
},
},
attributionData: {},
currentDate: "2000-01-01T10:00:0.001Z",
profileAgeCreated: {},
profileAgeReset: {},
usesFirefoxSync: false,
isFxAEnabled: true,
isFxASignedIn: false,
sync: {
desktopDevices: 0,
mobileDevices: 0,
totalDevices: 0,
},
xpinstallEnabled: true,
addonsInfo: {},
searchEngines: {},
isDefaultBrowser: false,
devToolsOpenedCount: 5,
topFrecentSites: {},
recentBookmarks: {},
pinnedSites: [
{
url: "https://amazon.com",
host: "amazon.com",
searchTopSite: true,
},
],
providerCohorts: {
onboarding: "",
cfr: "",
"message-groups": "",
"messaging-experiments": "",
"whats-new-panel": "",
},
totalBookmarksCount: {},
firefoxVersion: 80,
region: "US",
needsUpdate: {},
hasPinnedTabs: false,
hasAccessedFxAPanel: false,
isWhatsNewPanelEnabled: true,
userPrefs: {
cfrFeatures: true,
cfrAddons: true,
},
totalBlockedCount: {},
blockedCountByType: {},
attachedFxAOAuthClients: [],
platformName: "macosx",
scores: {},
scoreThreshold: 5000,
isChinaRepack: false,
userId: "adsf",
},
};
ASRouterPreferences.specialConditions = {
someCondition: true,
};
sandbox.spy(ASRouterPreferences, "init");
sandbox.spy(ASRouterPreferences, "uninit");
sandbox.spy(ASRouterPreferences, "addListener");
sandbox.spy(ASRouterPreferences, "removeListener");
clock = sandbox.useFakeTimers();
fetchStub = sandbox
.stub(global, "fetch")
.withArgs("http://fake.com/endpoint")
.resolves({
ok: true,
status: 200,
json: () => Promise.resolve({ messages: FAKE_REMOTE_MESSAGES }),
headers: FAKE_RESPONSE_HEADERS,
});
sandbox.stub(global.Services.prefs, "getStringPref");
fakeAttributionCode = {
allowedCodeKeys: ["foo", "bar", "baz"],
_clearCache: () => sinon.stub(),
getAttrDataAsync: () => Promise.resolve({ content: "addonID" }),
deleteFileAsync: () => Promise.resolve(),
writeAttributionFile: () => Promise.resolve(),
getCachedAttributionData: sinon.stub(),
};
FakeToolbarPanelHub = {
init: sandbox.stub(),
uninit: sandbox.stub(),
forceShowMessage: sandbox.stub(),
enableToolbarButton: sandbox.stub(),
};
FakeToolbarBadgeHub = {
init: sandbox.stub(),
uninit: sandbox.stub(),
registerBadgeNotificationListener: sandbox.stub(),
};
FakeMomentsPageHub = {
init: sandbox.stub(),
uninit: sandbox.stub(),
executeAction: sandbox.stub(),
};
fakeTargetingContext = {
combineContexts: sandbox.stub(),
evalWithDefault: sandbox.stub().resolves(),
};
let fakeNimbusFeatures = [
"cfr",
"infobar",
"spotlight",
"moments-page",
"pbNewtab",
].reduce((features, featureId) => {
features[featureId] = {
getAllVariables: sandbox.stub().returns(null),
recordExposureEvent: sandbox.stub(),
};
return features;
}, {});
globals.set({
// Testing framework doesn't know how to `defineLazyModuleGetters` so we're
// importing these modules into the global scope ourselves.
GroupsConfigurationProvider: { getMessages: () => Promise.resolve([]) },
ASRouterPreferences,
TARGETING_PREFERENCES,
ASRouterTargeting,
ASRouterTriggerListeners,
QueryCache,
gBrowser: { selectedBrowser: {} },
gURLBar: {},
isSeparateAboutWelcome: true,
AttributionCode: fakeAttributionCode,
PanelTestProvider,
MacAttribution: { applicationPath: "" },
ToolbarBadgeHub: FakeToolbarBadgeHub,
ToolbarPanelHub: FakeToolbarPanelHub,
MomentsPageHub: FakeMomentsPageHub,
KintoHttpClient: class {
bucket() {
return this;
}
collection() {
return this;
}
getRecord() {
return Promise.resolve({ data: {} });
}
},
Downloader: class {
download() {
return Promise.resolve("/path/to/download");
}
},
NimbusFeatures: fakeNimbusFeatures,
ExperimentAPI: {
getExperimentMetaData: sandbox.stub().returns({
slug: "experiment-slug",
active: true,
branch: { slug: "experiment-branch-slug" },
}),
getExperiment: sandbox.stub().returns({
branch: {
slug: "unit-slug",
feature: {
featureId: "foo",
value: { id: "test-message" },
},
},
}),
getAllBranches: sandbox.stub().resolves([]),
ready: sandbox.stub().resolves(),
},
SpecialMessageActions: {
handleAction: sandbox.stub(),
},
TargetingContext: class {
static combineContexts(...args) {
return fakeTargetingContext.combineContexts.apply(sandbox, args);
}
evalWithDefault(expr) {
return fakeTargetingContext.evalWithDefault(expr);
}
},
RemoteL10n: {
// This is just a subset of supported locales that happen to be used in
// the test.
isLocaleSupported: locale => ["en-US", "ja-JP-mac"].includes(locale),
},
});
await createRouterAndInit();
});
afterEach(() => {
Router.uninit();
ASRouterPreferences.uninit();
sandbox.restore();
globals.restore();
});
describe(".state", () => {
it("should throw if an attempt to set .state was made", () => {
assert.throws(() => {
Router.state = {};
});
});
});
describe("#init", () => {
it("should only be called once", async () => {
Router = new _ASRouter();
let state = await initASRouter(Router);
assert.equal(state, Router.state);
assert.isNull(await Router.init({}));
});
it("should only be called once", async () => {
Router = new _ASRouter();
initASRouter(Router);
let secondCall = await Router.init({});
assert.isNull(
secondCall,
"Should not init twice, it should exit early with null"
);
});
it("should set state.messageBlockList to the block list in persistent storage", async () => {
messageBlockList = ["foo"];
Router = new _ASRouter();
await initASRouter(Router);
assert.deepEqual(Router.state.messageBlockList, ["foo"]);
});
it("should initialize all the hub providers", async () => {
// ASRouter init called in `beforeEach` block above
assert.calledOnce(FakeToolbarBadgeHub.init);
assert.calledOnce(FakeToolbarPanelHub.init);
assert.calledOnce(FakeMomentsPageHub.init);
assert.calledWithExactly(
FakeToolbarBadgeHub.init,
Router.waitForInitialized,
{
handleMessageRequest: Router.handleMessageRequest,
addImpression: Router.addImpression,
blockMessageById: Router.blockMessageById,
sendTelemetry: Router.sendTelemetry,
unblockMessageById: Router.unblockMessageById,
}
);
assert.calledWithExactly(
FakeToolbarPanelHub.init,
Router.waitForInitialized,
{
getMessages: Router.handleMessageRequest,
sendTelemetry: Router.sendTelemetry,
}
);
assert.calledWithExactly(
FakeMomentsPageHub.init,
Router.waitForInitialized,
{
handleMessageRequest: Router.handleMessageRequest,
addImpression: Router.addImpression,
blockMessageById: Router.blockMessageById,
sendTelemetry: Router.sendTelemetry,
}
);
});
it("should set state.messageImpressions to the messageImpressions object in persistent storage", async () => {
// Note that messageImpressions are only kept if a message exists in router and has a .frequency property,
// otherwise they will be cleaned up by .cleanupImpressions()
const testMessage = { id: "foo", frequency: { lifetimeCap: 10 } };
messageImpressions = { foo: [0, 1, 2] };
setMessageProviderPref([
{ id: "onboarding", type: "local", messages: [testMessage] },
]);
Router = new _ASRouter();
await initASRouter(Router);
assert.deepEqual(Router.state.messageImpressions, messageImpressions);
});
it("should set state.screenImpressions to the screenImpressions object in persistent storage", async () => {
screenImpressions = { test: 123 };
Router = new _ASRouter();
await initASRouter(Router);
assert.deepEqual(Router.state.screenImpressions, screenImpressions);
});
it("should clear impressions for groups that are not active", async () => {
groupImpressions = { foo: [0, 1, 2] };
Router = new _ASRouter();
await initASRouter(Router);
assert.notProperty(Router.state.groupImpressions, "foo");
});
it("should keep impressions for groups that are active", async () => {
Router = new _ASRouter();
await initASRouter(Router);
await Router.setState(() => {
return {
groups: [
{
id: "foo",
enabled: true,
frequency: {
custom: [{ period: ONE_DAY_IN_MS, cap: 10 }],
lifetime: Infinity,
},
},
],
groupImpressions: { foo: [Date.now()] },
};
});
Router.cleanupImpressions();
assert.property(Router.state.groupImpressions, "foo");
assert.lengthOf(Router.state.groupImpressions.foo, 1);
});
it("should remove old impressions for a group", async () => {
Router = new _ASRouter();
await initASRouter(Router);
await Router.setState(() => {
return {
groups: [
{
id: "foo",
enabled: true,
frequency: {
custom: [{ period: ONE_DAY_IN_MS, cap: 10 }],
},
},
],
groupImpressions: {
foo: [Date.now() - ONE_DAY_IN_MS - 1, Date.now()],
},
};
});
Router.cleanupImpressions();
assert.property(Router.state.groupImpressions, "foo");
assert.lengthOf(Router.state.groupImpressions.foo, 1);
});
it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
await initASRouter(Router);
assert.calledOnce(Router.loadMessagesFromAllProviders);
assert.isArray(Router.state.messages);
assert.lengthOf(
Router.state.messages,
FAKE_LOCAL_MESSAGES.length + FAKE_REMOTE_MESSAGES.length
);
});
it("should set state.previousSessionEnd from IndexedDB", async () => {
previousSessionEnd = 200;
await createRouterAndInit();
assert.equal(Router.state.previousSessionEnd, previousSessionEnd);
});
it("should assign ASRouterPreferences.specialConditions to state", async () => {
assert.isTrue(ASRouterPreferences.specialConditions.someCondition);
assert.isTrue(Router.state.someCondition);
});
it("should add observer for `intl:app-locales-changed`", async () => {
sandbox.spy(global.Services.obs, "addObserver");
await createRouterAndInit();
assert.calledWithExactly(
global.Services.obs.addObserver,
Router._onLocaleChanged,
"intl:app-locales-changed"
);
});
it("should add a pref observer", async () => {
sandbox.spy(global.Services.prefs, "addObserver");
await createRouterAndInit();
assert.calledOnce(global.Services.prefs.addObserver);
assert.calledWithExactly(
global.Services.prefs.addObserver,
USE_REMOTE_L10N_PREF,
Router
);
});
describe("lazily loading local test providers", () => {
afterEach(() => {
Router.uninit();
});
it("should add the local test providers on init if devtools are enabled", async () => {
sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
await createRouterAndInit();
assert.property(Router._localProviders, "PanelTestProvider");
});
it("should not add the local test providers on init if devtools are disabled", async () => {
sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
await createRouterAndInit();
assert.notProperty(Router._localProviders, "PanelTestProvider");
});
});
});
describe("preference changes", () => {
it("should call ASRouterPreferences.init and add a listener on init", () => {
assert.calledOnce(ASRouterPreferences.init);
assert.calledWith(ASRouterPreferences.addListener, Router.onPrefChange);
});
it("should call ASRouterPreferences.uninit and remove the listener on uninit", () => {
Router.uninit();
assert.calledOnce(ASRouterPreferences.uninit);
assert.calledWith(
ASRouterPreferences.removeListener,
Router.onPrefChange
);
});
it("should send a AS_ROUTER_TARGETING_UPDATE message", async () => {
const messageTargeted = {
id: "1",
campaign: "foocampaign",
targeting: "true",
groups: ["cfr"],
provider: "cfr",
};
const messageNotTargeted = {
id: "2",
campaign: "foocampaign",
groups: ["cfr"],
provider: "cfr",
};
await Router.setState({
messages: [messageTargeted, messageNotTargeted],
providers: [{ id: "cfr" }],
});
fakeTargetingContext.evalWithDefault.resolves(false);
await Router.onPrefChange("services.sync.username");
assert.calledOnce(initParams.clearChildMessages);
assert.calledWith(initParams.clearChildMessages, [messageTargeted.id]);
});
it("should call loadMessagesFromAllProviders on pref change", () => {
ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);
assert.calledOnce(Router.loadMessagesFromAllProviders);
});
it("should update groups state if a user pref changes", async () => {
await Router.setState({
groups: [{ id: "foo", userPreferences: ["bar"], enabled: true }],
});
sandbox.stub(ASRouterPreferences, "getUserPreference");
await Router.onPrefChange(MESSAGE_PROVIDER_PREF_NAME);
assert.calledWithExactly(ASRouterPreferences.getUserPreference, "bar");
});
it("should update the list of providers on pref change", async () => {
const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {
url: "baz.com",
});
setMessageProviderPref([
FAKE_LOCAL_PROVIDER,
modifiedRemoteProvider,
FAKE_REMOTE_SETTINGS_PROVIDER,
]);
const { length } = Router.state.providers;
ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);
await Router._updateMessageProviders();
const provider = Router.state.providers.find(p => p.url === "baz.com");
assert.lengthOf(Router.state.providers, length);
assert.isDefined(provider);
});
it("should clear disabled providers on pref change", async () => {
const TEST_PROVIDER_ID = "some_provider_id";
await Router.setState({
providers: [{ id: TEST_PROVIDER_ID }],
});
const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {
id: TEST_PROVIDER_ID,
enabled: false,
});
setMessageProviderPref([
FAKE_LOCAL_PROVIDER,
modifiedRemoteProvider,
FAKE_REMOTE_SETTINGS_PROVIDER,
]);
await Router.onPrefChange(MESSAGE_PROVIDER_PREF_NAME);
assert.calledOnce(initParams.clearChildProviders);
assert.calledWith(initParams.clearChildProviders, [TEST_PROVIDER_ID]);
});
});
describe("setState", () => {
it("should broadcast a message to update the admin tool on a state change if the asrouter.devtoolsEnabled pref is", async () => {
sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
sandbox.stub(Router, "getTargetingParameters").resolves({});
const state = await Router.setState({ foo: 123 });
assert.calledOnce(initParams.updateAdminState);
assert.deepEqual(state.providerPrefs, ASRouterPreferences.providers);
assert.deepEqual(
state.userPrefs,
ASRouterPreferences.getAllUserPreferences()
);
assert.deepEqual(state.targetingParameters, {});
assert.deepEqual(state.errors, Router.errors);
});
it("should not send a message on a state change asrouter.devtoolsEnabled pref is on", async () => {
sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
await Router.setState({ foo: 123 });
assert.notCalled(initParams.updateAdminState);
});
});
describe("getTargetingParameters", () => {
it("should return the targeting parameters", async () => {
const stub = sandbox.stub().resolves("foo");
const obj = { foo: 1 };
sandbox.stub(obj, "foo").get(stub);
const result = await Router.getTargetingParameters(obj, obj);
assert.calledTwice(stub);
assert.propertyVal(result, "foo", "foo");
});
});
describe("evaluateExpression", () => {
it("should call ASRouterTargeting to evaluate", async () => {
fakeTargetingContext.evalWithDefault.resolves("foo");
const response = await Router.evaluateExpression({});
assert.equal(response.evaluationStatus.result, "foo");
assert.isTrue(response.evaluationStatus.success);
});
it("should catch evaluation errors", async () => {
fakeTargetingContext.evalWithDefault.returns(
Promise.reject(new Error("fake error"))
);
const response = await Router.evaluateExpression({});
assert.isFalse(response.evaluationStatus.success);
});
});
describe("#routeCFRMessage", () => {
let browser;
beforeEach(() => {
sandbox.stub(CFRPageActions, "forceRecommendation");
sandbox.stub(CFRPageActions, "addRecommendation");
browser = {};
});
it("should route whatsnew_panel_message message to the right hub", () => {
Router.routeCFRMessage(
{ template: "whatsnew_panel_message" },
browser,
"",
true
);
assert.calledOnce(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(FakeMomentsPageHub.executeAction);
});
it("should route moments messages to the right hub", () => {
Router.routeCFRMessage({ template: "update_action" }, browser, "", true);
assert.calledOnce(FakeMomentsPageHub.executeAction);
assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(CFRPageActions.forceRecommendation);
});
it("should route toolbar_badge message to the right hub", () => {
Router.routeCFRMessage({ template: "toolbar_badge" }, browser);
assert.calledOnce(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(FakeMomentsPageHub.executeAction);
});
it("should route milestone_message to the right hub", () => {
Router.routeCFRMessage(
{ template: "milestone_message" },
browser,
"",
false
);
assert.calledOnce(CFRPageActions.addRecommendation);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(FakeMomentsPageHub.executeAction);
});
it("should route cfr_doorhanger message to the right hub force = false", () => {
Router.routeCFRMessage(
{ template: "cfr_doorhanger" },
browser,
{ param: {} },
false
);
assert.calledOnce(CFRPageActions.addRecommendation);
assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(FakeMomentsPageHub.executeAction);
});
it("should route cfr_doorhanger message to the right hub force = true", () => {
Router.routeCFRMessage({ template: "cfr_doorhanger" }, browser, {}, true);
assert.calledOnce(CFRPageActions.forceRecommendation);
assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(FakeMomentsPageHub.executeAction);
});
it("should route cfr_urlbar_chiclet message to the right hub force = false", () => {
Router.routeCFRMessage(
{ template: "cfr_urlbar_chiclet" },
browser,
{ param: {} },
false
);
assert.calledOnce(CFRPageActions.addRecommendation);
const { args } = CFRPageActions.addRecommendation.firstCall;
// Host should be null
assert.isNull(args[1]);
assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(FakeMomentsPageHub.executeAction);
});
it("should route cfr_urlbar_chiclet message to the right hub force = true", () => {
Router.routeCFRMessage(
{ template: "cfr_urlbar_chiclet" },
browser,
{},
true
);
assert.calledOnce(CFRPageActions.forceRecommendation);
assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(FakeMomentsPageHub.executeAction);
});
it("should route default to sending to content", () => {
Router.routeCFRMessage(
{ template: "some_other_template" },
browser,
{},
true
);
assert.notCalled(FakeToolbarPanelHub.forceShowMessage);
assert.notCalled(CFRPageActions.forceRecommendation);
assert.notCalled(CFRPageActions.addRecommendation);
assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);
assert.notCalled(FakeMomentsPageHub.executeAction);
});
});
describe("#loadMessagesFromAllProviders", () => {
function assertRouterContainsMessages(messages) {
const messageIdsInRouter = Router.state.messages.map(m => m.id);
for (const message of messages) {
assert.include(messageIdsInRouter, message.id);
}
}
it("should not trigger an update if not enough time has passed for a provider", async () => {
await createRouterAndInit([
{
id: "remotey",
type: "remote",
enabled: true,
url: "http://fake.com/endpoint",
updateCycleInMs: 300,
},
]);
const previousState = Router.state;
// Since we've previously gotten messages during init and we haven't advanced our fake timer,
// no updates should be triggered.
await Router.loadMessagesFromAllProviders();
assert.deepEqual(Router.state, previousState);
});
it("should not trigger an update if we only have local providers", async () => {
await createRouterAndInit([
{
id: "foo",
type: "local",
enabled: true,
messages: FAKE_LOCAL_MESSAGES,
},
]);
const previousState = Router.state;
const stub = sandbox.stub(MessageLoaderUtils, "loadMessagesForProvider");
clock.tick(300);
await Router.loadMessagesFromAllProviders();
assert.deepEqual(Router.state, previousState);
assert.notCalled(stub);
});
it("should update messages for a provider if enough time has passed, without removing messages for other providers", async () => {
const NEW_MESSAGES = [{ id: "new_123" }];
await createRouterAndInit([
{
id: "remotey",
type: "remote",
url: "http://fake.com/endpoint",
enabled: true,
updateCycleInMs: 300,
},
{
id: "alocalprovider",
type: "local",
enabled: true,
messages: FAKE_LOCAL_MESSAGES,
},
]);
fetchStub.withArgs("http://fake.com/endpoint").resolves({
ok: true,
status: 200,
json: () => Promise.resolve({ messages: NEW_MESSAGES }),
headers: FAKE_RESPONSE_HEADERS,
});
clock.tick(301);
await Router.loadMessagesFromAllProviders();
// These are the new messages
assertRouterContainsMessages(NEW_MESSAGES);
// These are the local messages that should not have been deleted
assertRouterContainsMessages(FAKE_LOCAL_MESSAGES);
});
it("should parse the triggers in the messages and register the trigger listeners", async () => {
sandbox.spy(
ASRouterTriggerListeners.get("openURL"),
"init"
); /* eslint-disable object-property-newline */
/* eslint-disable object-curly-newline */ await createRouterAndInit([
{
id: "foo",
type: "local",
enabled: true,
messages: [
{
id: "foo",
template: "simple_template",
trigger: { id: "firstRun" },
content: { title: "Foo", body: "Foo123" },
},
{
id: "bar1",
template: "simple_template",
trigger: {
id: "openURL",
params: ["www.mozilla.org", "www.mozilla.com"],
},
content: { title: "Bar1", body: "Bar123" },
},
{
id: "bar2",
template: "simple_template",
trigger: { id: "openURL", params: ["www.example.com"] },
content: { title: "Bar2", body: "Bar123" },
},
],
},
]); /* eslint-enable object-property-newline */
/* eslint-enable object-curly-newline */ assert.calledTwice(
ASRouterTriggerListeners.get("openURL").init
);
assert.calledWithExactly(
ASRouterTriggerListeners.get("openURL").init,
Router._triggerHandler,
["www.mozilla.org", "www.mozilla.com"],
undefined
);
assert.calledWithExactly(
ASRouterTriggerListeners.get("openURL").init,
Router._triggerHandler,
["www.example.com"],
undefined
);
});
it("should parse the message's messagesLoaded trigger and immediately fire trigger", async () => {
setMessageProviderPref([
{
id: "foo",
type: "local",
enabled: true,
messages: [
{
id: "bar3",
template: "simple_template",
trigger: { id: "messagesLoaded" },
content: { title: "Bar3", body: "Bar123" },
},
],
},
]);
Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
sandbox.spy(Router, "sendTriggerMessage");
await initASRouter(Router);
assert.calledOnce(Router.sendTriggerMessage);
assert.calledWith(
Router.sendTriggerMessage,
sandbox.match({ id: "messagesLoaded" }),
true
);
});
it("should gracefully handle messages loading before a window or browser exists", async () => {
sandbox.stub(global, "gBrowser").value(undefined);
sandbox
.stub(global.Services.wm, "getMostRecentBrowserWindow")
.returns(undefined);
setMessageProviderPref([
{
id: "foo",
type: "local",
enabled: true,
messages: [
"whatsnew_panel_message",
"cfr_doorhanger",
"toolbar_badge",
"update_action",
"infobar",
"spotlight",
"toast_notification",
].map((template, i) => {
return {
id: `foo${i}`,
template,
trigger: { id: "messagesLoaded" },
content: { title: `Foo${i}`, body: "Bar123" },
};
}),
},
]);
Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));
sandbox.spy(Router, "sendTriggerMessage");
await initASRouter(Router);
assert.calledWith(
Router.sendTriggerMessage,
sandbox.match({ id: "messagesLoaded" }),
true
);
});
it("should gracefully handle RemoteSettings blowing up and dispatch undesired event", async () => {
sandbox
.stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
.rejects("fake error");
await createRouterAndInit();
assert.calledWith(initParams.dispatchCFRAction, {
data: {
action: "asrouter_undesired_event",
event: "ASR_RS_ERROR",
event_context: "remotey-settingsy",
message_id: "n/a",
},
meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
type: "AS_ROUTER_TELEMETRY_USER_EVENT",
});
});
it("should dispatch undesired event if RemoteSettings returns no messages", async () => {
sandbox
.stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
.resolves([]);
assert.calledWith(initParams.dispatchCFRAction, {
data: {
action: "asrouter_undesired_event",
event: "ASR_RS_NO_MESSAGES",
event_context: "remotey-settingsy",
message_id: "n/a",
},
meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
type: "AS_ROUTER_TELEMETRY_USER_EVENT",
});
});
it("should download the attachment if RemoteSettings returns some messages", async () => {
sandbox
.stub(global.Services.locale, "appLocaleAsBCP47")
.get(() => "en-US");
sandbox
.stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
.resolves([{ id: "message_1" }]);
const spy = sandbox.spy();
global.Downloader.prototype.downloadToDisk = spy;
const provider = {
id: "cfr",
enabled: true,
type: "remote-settings",
collection: "cfr",
};
await createRouterAndInit([provider]);
assert.calledOnce(spy);
});
it("should dispatch undesired event if the ms-language-packs returns no messages", async () => {
sandbox
.stub(global.Services.locale, "appLocaleAsBCP47")
.get(() => "en-US");
sandbox
.stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
.resolves([{ id: "message_1" }]);
sandbox
.stub(global.KintoHttpClient.prototype, "getRecord")
.resolves(null);
const provider = {
id: "cfr",
enabled: true,
type: "remote-settings",
collection: "cfr",
};
await createRouterAndInit([provider]);
assert.calledWith(initParams.dispatchCFRAction, {
data: {
action: "asrouter_undesired_event",
event: "ASR_RS_NO_MESSAGES",
event_context: "ms-language-packs",
message_id: "n/a",
},
meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" },
type: "AS_ROUTER_TELEMETRY_USER_EVENT",
});
});
});
describe("#_updateMessageProviders", () => {
it("should correctly replace %STARTPAGE_VERSION% in remote provider urls", async () => {
// If this test fails, you need to update the constant STARTPAGE_VERSION in
// ASRouter.jsm to match the `version` property of provider-response-schema.json
const expectedStartpageVersion = ProviderResponseSchema.version;
const provider = {
id: "foo",
enabled: true,
type: "remote",
url: "https://www.mozilla.org/%STARTPAGE_VERSION%/",
};
setMessageProviderPref([provider]);
await Router._updateMessageProviders();
assert.equal(
Router.state.providers[0].url,
`https://www.mozilla.org/${parseInt(expectedStartpageVersion, 10)}/`
);
});
it("should replace other params in remote provider urls by calling Services.urlFormater.formatURL", async () => {
const url = "https://www.example.com/";
const replacedUrl = "https://www.foo.bar/";
const stub = sandbox
.stub(global.Services.urlFormatter, "formatURL")
.withArgs(url)
.returns(replacedUrl);
const provider = { id: "foo", enabled: true, type: "remote", url };
setMessageProviderPref([provider]);
await Router._updateMessageProviders();
assert.calledOnce(stub);
assert.calledWithExactly(stub, url);
assert.equal(Router.state.providers[0].url, replacedUrl);
});
it("should only add the providers that are enabled", async () => {
const providers = [
{
id: "foo",
enabled: false,
type: "remote",
url: "https://www.foo.com/",
},
{
id: "bar",
enabled: true,
type: "remote",
url: "https://www.bar.com/",
},
];
setMessageProviderPref(providers);
await Router._updateMessageProviders();
assert.equal(Router.state.providers.length, 1);
assert.equal(Router.state.providers[0].id, providers[1].id);
});
});
describe("#handleMessageRequest", () => {
beforeEach(async () => {
await Router.setState(() => ({
providers: [{ id: "cfr" }, { id: "badge" }],
}));
});
it("should not return a blocked message", async () => {
// Block all messages except the first
await Router.setState(() => ({
messages: [
{ id: "foo", provider: "cfr", groups: ["cfr"] },
{ id: "bar", provider: "cfr", groups: ["cfr"] },
],
messageBlockList: ["foo"],
}));
await Router.handleMessageRequest({
provider: "cfr",
});
assert.calledWithMatch(ASRouterTargeting.findMatchingMessage, {
messages: [{ id: "bar", provider: "cfr", groups: ["cfr"] }],
});
});
it("should not return a message from a disabled group", async () => {
ASRouterTargeting.findMatchingMessage.callsFake(
({ messages }) => messages[0]
);
// Block all messages except the first
await Router.setState(() => ({
messages: [
{ id: "foo", provider: "cfr", groups: ["cfr"] },
{ id: "bar", provider: "cfr", groups: ["cfr"] },
],
groups: [{ id: "cfr", enabled: false }],
}));
const result = await Router.handleMessageRequest({
provider: "cfr",
});
assert.isNull(result);
});
it("should not return a message from a blocked campaign", async () => {
// Block all messages except the first
await Router.setState(() => ({
messages: [
{
id: "foo",
provider: "cfr",
campaign: "foocampaign",
groups: ["cfr"],
},
{ id: "bar", provider: "cfr", groups: ["cfr"] },
],
messageBlockList: ["foocampaign"],
}));
await Router.handleMessageRequest({
provider: "cfr",
});
assert.calledWithMatch(ASRouterTargeting.findMatchingMessage, {
messages: [{ id: "bar", provider: "cfr", groups: ["cfr"] }],
});
});
it("should not return a message excluded by the provider", async () => {
// There are only two providers; block the FAKE_LOCAL_PROVIDER, leaving
// only FAKE_REMOTE_PROVIDER unblocked, which provides only one message
await Router.setState(() => ({
providers: [{ id: "cfr", exclude: ["foo"] }],
}));
await Router.setState(() => ({
messages: [{ id: "foo", provider: "cfr" }],
messageBlockList: ["foocampaign"],
}));
const result = await Router.handleMessageRequest({
provider: "cfr",
});
assert.isNull(result);
});
it("should not return a message if the frequency cap has been hit", async () => {
sandbox.stub(Router, "isBelowFrequencyCaps").returns(false);
await Router.setState(() => ({
messages: [{ id: "foo", provider: "cfr" }],
}));
const result = await Router.handleMessageRequest({
provider: "cfr",
});
assert.isNull(result);
});
it("should get unblocked messages that match the trigger", async () => {
const message1 = {
id: "1",
campaign: "foocampaign",
trigger: { id: "foo" },
groups: ["cfr"],
provider: "cfr",
};
const message2 = {
id: "2",
campaign: "foocampaign",
trigger: { id: "bar" },
groups: ["cfr"],
provider: "cfr",
};
await Router.setState({ messages: [message2, message1] });
// Just return the first message provided as arg
ASRouterTargeting.findMatchingMessage.callsFake(
({ messages }) => messages[0]
);
const result = Router.handleMessageRequest({ triggerId: "foo" });
assert.deepEqual(result, message1);
});
it("should get unblocked messages that match trigger and template", async () => {
const message1 = {
id: "1",
campaign: "foocampaign",
template: "badge",
trigger: { id: "foo" },
groups: ["badge"],
provider: "badge",
};
const message2 = {
id: "2",
campaign: "foocampaign",
template: "test_template",
trigger: { id: "foo" },
groups: ["cfr"],
provider: "cfr",
};
await Router.setState({ messages: [message2, message1] });
// Just return the first message provided as arg
ASRouterTargeting.findMatchingMessage.callsFake(
({ messages }) => messages[0]
);
const result = Router.handleMessageRequest({
triggerId: "foo",
template: "badge",
});
assert.deepEqual(result, message1);
});
it("should have messageImpressions in the message context", () => {
assert.propertyVal(
Router._getMessagesContext(),
"messageImpressions",
Router.state.messageImpressions
);
});
it("should return all unblocked messages that match the template, trigger if returnAll=true", async () => {
const message1 = {
provider: "whats_new",
id: "1",
template: "whatsnew_panel_message",
trigger: { id: "whatsNewPanelOpened" },
groups: ["whats_new"],
};
const message2 = {
provider: "whats_new",
id: "2",
template: "whatsnew_panel_message",
trigger: { id: "whatsNewPanelOpened" },
groups: ["whats_new"],
};
const message3 = {
provider: "whats_new",
id: "3",
template: "badge",
groups: ["whats_new"],
};
ASRouterTargeting.findMatchingMessage.callsFake(() => [
message2,
message1,
]);
await Router.setState({
messages: [message3, message2, message1],
providers: [{ id: "whats_new" }],
});
const result = await Router.handleMessageRequest({
template: "whatsnew_panel_message",
triggerId: "whatsNewPanelOpened",
returnAll: true,
});
assert.deepEqual(result, [message2, message1]);
});
it("should forward trigger param info", async () => {
const trigger = {
triggerId: "foo",
triggerParam: "bar",
triggerContext: "context",
};
const message1 = {
id: "1",
campaign: "foocampaign",
trigger: { id: "foo" },
groups: ["cfr"],
provider: "cfr",
};
const message2 = {
id: "2",
campaign: "foocampaign",
trigger: { id: "bar" },
groups: ["badge"],
provider: "badge",
};
await Router.setState({ messages: [message2, message1] });
// Just return the first message provided as arg
Router.handleMessageRequest(trigger);
assert.calledOnce(ASRouterTargeting.findMatchingMessage);
const [options] = ASRouterTargeting.findMatchingMessage.firstCall.args;
assert.propertyVal(options.trigger, "id", trigger.triggerId);
assert.propertyVal(options.trigger, "param", trigger.triggerParam);
assert.propertyVal(options.trigger, "context", trigger.triggerContext);
});
it("should not cache badge messages", async () => {
const trigger = {
triggerId: "bar",
triggerParam: "bar",
triggerContext: "context",
};
const message1 = {
id: "1",
provider: "cfr",
campaign: "foocampaign",
trigger: { id: "foo" },
groups: ["cfr"],
};
const message2 = {
id: "2",
campaign: "foocampaign",
trigger: { id: "bar" },
groups: ["badge"],
provider: "badge",
};
await Router.setState({ messages: [message2, message1] });
// Just return the first message provided as arg
Router.handleMessageRequest(trigger);
assert.calledOnce(ASRouterTargeting.findMatchingMessage);
const [options] = ASRouterTargeting.findMatchingMessage.firstCall.args;
assert.propertyVal(options, "shouldCache", false);
});
it("should filter out messages without a trigger (or different) when a triggerId is defined", async () => {
const trigger = { triggerId: "foo" };
const message1 = {
id: "1",
campaign: "foocampaign",
trigger: { id: "foo" },
groups: ["cfr"],
provider: "cfr",
};
const message2 = {
id: "2",
campaign: "foocampaign",
trigger: { id: "bar" },
groups: ["cfr"],
provider: "cfr",
};
const message3 = {
id: "3",
campaign: "bazcampaign",
groups: ["cfr"],
provider: "cfr",
};
await Router.setState({
messages: [message2, message1, message3],
groups: [{ id: "cfr", enabled: true }],
});
// Just return the first message provided as arg
ASRouterTargeting.findMatchingMessage.callsFake(args => args.messages);
const result = Router.handleMessageRequest(trigger);
assert.lengthOf(result, 1);
assert.deepEqual(result[0], message1);
});
});
describe("#uninit", () => {
it("should unregister the trigger listeners", () => {
for (const listener of ASRouterTriggerListeners.values()) {
sandbox.spy(listener, "uninit");
}
Router.uninit();
for (const listener of ASRouterTriggerListeners.values()) {
assert.calledOnce(listener.uninit);
}
});
it("should set .dispatchCFRAction to null", () => {
Router.uninit();
assert.isNull(Router.dispatchCFRAction);
assert.isNull(Router.clearChildMessages);
assert.isNull(Router.sendTelemetry);
});
it("should save previousSessionEnd", () => {
Router.uninit();
assert.calledOnce(Router._storage.set);
assert.calledWithExactly(
Router._storage.set,
"previousSessionEnd",
sinon.match.number
);
});
it("should remove the observer for `intl:app-locales-changed`", () => {
sandbox.spy(global.Services.obs, "removeObserver");
Router.uninit();
assert.calledWithExactly(
global.Services.obs.removeObserver,
Router._onLocaleChanged,
"intl:app-locales-changed"
);
});
it("should remove the pref observer for `USE_REMOTE_L10N_PREF`", async () => {
sandbox.spy(global.Services.prefs, "removeObserver");
Router.uninit();
// Grab the last call as #uninit() also involves multiple calls of `Services.prefs.removeObserver`.
const call = global.Services.prefs.removeObserver.lastCall;
assert.calledWithExactly(call, USE_REMOTE_L10N_PREF, Router);
});
});
describe("#setMessageById", async () => {
it("should send an empty message if provided id did not resolve to a message", async () => {
let response = await Router.setMessageById({ id: -1 }, true, {});
assert.deepEqual(response.message, {});
});
});
describe("#isUnblockedMessage", () => {
it("should block a message if the group is blocked", async () => {
const msg = { id: "msg1", groups: ["foo"], provider: "unit-test" };
await Router.setState({
groups: [{ id: "foo", enabled: false }],
messages: [msg],
providers: [{ id: "unit-test" }],
});
assert.isFalse(Router.isUnblockedMessage(msg));
await Router.setState({ groups: [{ id: "foo", enabled: true }] });
assert.isTrue(Router.isUnblockedMessage(msg));
});
it("should block a message if at least one group is blocked", async () => {
const msg = {
id: "msg1",
groups: ["foo", "bar"],
provider: "unit-test",
};
await Router.setState({
groups: [
{ id: "foo", enabled: false },
{ id: "bar", enabled: false },
],
messages: [msg],
providers: [{ id: "unit-test" }],
});
assert.isFalse(Router.isUnblockedMessage(msg));
await Router.setState({
groups: [
{ id: "foo", enabled: true },
{ id: "bar", enabled: false },
],
});
assert.isFalse(Router.isUnblockedMessage(msg));
});
});
describe("#blockMessageById", () => {
it("should add the id to the messageBlockList", async () => {
await Router.blockMessageById("foo");
assert.isTrue(Router.state.messageBlockList.includes("foo"));
});
it("should add the campaign to the messageBlockList instead of id if .campaign is specified and not select messages of that campaign again", async () => {
await Router.setState({
messages: [
{ id: "1", campaign: "foocampaign" },
{ id: "2", campaign: "foocampaign" },
],
});
await Router.blockMessageById("1");
assert.isTrue(Router.state.messageBlockList.includes("foocampaign"));
assert.isEmpty(Router.state.messages.filter(Router.isUnblockedMessage));
});
it("should be able to add multiple items to the messageBlockList", async () => {
await await Router.blockMessageById(FAKE_BUNDLE.map(b => b.id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
});
it("should save the messageBlockList", async () => {
await await Router.blockMessageById(FAKE_BUNDLE.map(b => b.id));
assert.calledWithExactly(Router._storage.set, "messageBlockList", [
FAKE_BUNDLE[0].id,
FAKE_BUNDLE[1].id,
]);
});
});
describe("#unblockMessageById", () => {
it("should remove the id from the messageBlockList", async () => {
await Router.blockMessageById("foo");
assert.isTrue(Router.state.messageBlockList.includes("foo"));
await Router.unblockMessageById("foo");
assert.isFalse(Router.state.messageBlockList.includes("foo"));
});
it("should remove the campaign from the messageBlockList if it is defined", async () => {
await Router.setState({ messages: [{ id: "1", campaign: "foo" }] });
await Router.blockMessageById("1");
assert.isTrue(
Router.state.messageBlockList.includes("foo"),
"blocklist has campaign id"
);
await Router.unblockMessageById("1");
assert.isFalse(
Router.state.messageBlockList.includes("foo"),
"campaign id removed from blocklist"
);
});
it("should save the messageBlockList", async () => {
await Router.unblockMessageById("foo");
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
});
});
describe("#routeCFRMessage", () => {
it("should allow for echoing back message modifications", () => {
const message = { somekey: "some value" };
const data = { content: message };
const browser = {};
let msg = Router.routeCFRMessage(data.content, browser, data, false);
assert.deepEqual(msg.message, message);
});
it("should call CFRPageActions.forceRecommendation if the template is cfr_action and force is true", async () => {
sandbox.stub(CFRPageActions, "forceRecommendation");
const testMessage = { id: "foo", template: "cfr_doorhanger" };
await Router.setState({ messages: [testMessage] });
Router.routeCFRMessage(testMessage, {}, null, true);
assert.calledOnce(CFRPageActions.forceRecommendation);
});
it("should call CFRPageActions.addRecommendation if the template is cfr_action and force is false", async () => {
sandbox.stub(CFRPageActions, "addRecommendation");
const testMessage = { id: "foo", template: "cfr_doorhanger" };
await Router.setState({ messages: [testMessage] });
Router.routeCFRMessage(testMessage, {}, {}, false);
assert.calledOnce(CFRPageActions.addRecommendation);
});
});
describe("#updateTargetingParameters", () => {
it("should return an object containing the whole state", async () => {
sandbox.stub(Router, "getTargetingParameters").resolves({});
let msg = await Router.updateTargetingParameters();
let expected = Object.assign({}, Router.state, {
providerPrefs: ASRouterPreferences.providers,
userPrefs: ASRouterPreferences.getAllUserPreferences(),
targetingParameters: {},
errors: Router.errors,
});
assert.deepEqual(msg, expected);
});
});
describe("#reachEvent", () => {
let experimentAPIStub;
let featureIds = ["cfr", "moments-page", "infobar", "spotlight"];
beforeEach(() => {
let getExperimentMetaDataStub = sandbox.stub();
let getAllBranchesStub = sandbox.stub();
featureIds.forEach(feature => {
global.NimbusFeatures[feature].getAllVariables.returns({
id: `message-${feature}`,
});
getExperimentMetaDataStub.withArgs({ featureId: feature }).returns({
slug: `slug-${feature}`,
branch: {
slug: `branch-${feature}`,
},
});
getAllBranchesStub.withArgs(`slug-${feature}`).resolves([
{
slug: `other-branch-${feature}`,
[feature]: { value: { trigger: "unit-test" } },
},
]);
});
experimentAPIStub = {
getExperimentMetaData: getExperimentMetaDataStub,
getAllBranches: getAllBranchesStub,
};
globals.set("ExperimentAPI", experimentAPIStub);
});
afterEach(() => {
sandbox.restore();
});
it("should tag `forReachEvent` for all the expected message types", async () => {
// This should match the `providers.messaging-experiments`
let response = await MessageLoaderUtils.loadMessagesForProvider({
type: "remote-experiments",
featureIds,
});
// 1 message for reach 1 for expose
assert.property(response, "messages");
assert.lengthOf(response.messages, featureIds.length * 2);
assert.lengthOf(
response.messages.filter(m => m.forReachEvent),
featureIds.length
);
});
});
describe("#sendTriggerMessage", () => {
it("should pass the trigger to ASRouterTargeting when sending trigger message", async () => {
await Router.setState({
messages: [
{
id: "foo1",
provider: "onboarding",
template: "onboarding",
trigger: { id: "firstRun" },
content: { title: "Foo1", body: "Foo123-1" },
groups: ["onboarding"],
},
],
providers: [{ id: "onboarding" }],
});
Router.loadMessagesFromAllProviders.resetHistory();
Router.loadMessagesFromAllProviders.onFirstCall().resolves();
await Router.sendTriggerMessage({
tabId: 0,
browser: {},
id: "firstRun",
});
assert.calledOnce(ASRouterTargeting.findMatchingMessage);
assert.deepEqual(
ASRouterTargeting.findMatchingMessage.firstCall.args[0].trigger,
{
id: "firstRun",
param: undefined,
context: undefined,
}
);
});
it("should record telemetry information", async () => {
const startTelemetryStopwatch = sandbox.stub(
global.TelemetryStopwatch,
"start"
);
const finishTelemetryStopwatch = sandbox.stub(
global.TelemetryStopwatch,
"finish"
);
const tabId = 123;
await Router.sendTriggerMessage({
tabId,
browser: {},
id: "firstRun",
});
assert.calledTwice(startTelemetryStopwatch);
assert.calledWithExactly(
startTelemetryStopwatch,
"MS_MESSAGE_REQUEST_TIME_MS",
{ tabId }
);
assert.calledTwice(finishTelemetryStopwatch);
assert.calledWithExactly(
finishTelemetryStopwatch,
"MS_MESSAGE_REQUEST_TIME_MS",
{ tabId }
);
});
it("should have previousSessionEnd in the message context", () => {
assert.propertyVal(
Router._getMessagesContext(),
"previousSessionEnd",
100
);
});
it("should record the Reach event if found any", async () => {
let messages = [
{
id: "foo1",
forReachEvent: { sent: false, group: "cfr" },
experimentSlug: "exp01",
branchSlug: "branch01",
template: "simple_template",
trigger: { id: "foo" },
content: { title: "Foo1", body: "Foo123-1" },
},
{
id: "foo2",
template: "simple_template",
trigger: { id: "bar" },
content: { title: "Foo2", body: "Foo123-2" },
provider: "onboarding",
},
{
id: "foo3",
forReachEvent: { sent: false, group: "cfr" },
experimentSlug: "exp02",
branchSlug: "branch02",
template: "simple_template",
trigger: { id: "foo" },
content: { title: "Foo1", body: "Foo123-1" },
},
];
sandbox.stub(Router, "handleMessageRequest").resolves(messages);
sandbox.spy(Services.telemetry, "recordEvent");
await Router.sendTriggerMessage({
tabId: 0,
browser: {},
id: "foo",
});
assert.calledTwice(Services.telemetry.recordEvent);
});
it("should not record the Reach event if it's already sent", async () => {
let messages = [
{
id: "foo1",
forReachEvent: { sent: true, group: "cfr" },
experimentSlug: "exp01",
branchSlug: "branch01",
template: "simple_template",
trigger: { id: "foo" },
content: { title: "Foo1", body: "Foo123-1" },
},
];
sandbox.stub(Router, "handleMessageRequest").resolves(messages);
sandbox.spy(Services.telemetry, "recordEvent");
await Router.sendTriggerMessage({
tabId: 0,
browser: {},
id: "foo",
});
assert.notCalled(Services.telemetry.recordEvent);
});
it("should record the Exposure event for each valid feature", async () => {
["cfr_doorhanger", "update_action", "infobar", "spotlight"].forEach(
async template => {
let featureMap = {
cfr_doorhanger: "cfr",
spotlight: "spotlight",
infobar: "infobar",
update_action: "moments-page",
};
assert.notCalled(
global.NimbusFeatures[featureMap[template]].recordExposureEvent
);
let messages = [
{
id: "foo1",
template,
trigger: { id: "foo" },
content: { title: "Foo1", body: "Foo123-1" },
},
];
sandbox.stub(Router, "handleMessageRequest").resolves(messages);
await Router.sendTriggerMessage({
tabId: 0,
browser: {},
id: "foo",
});
assert.calledOnce(
global.NimbusFeatures[featureMap[template]].recordExposureEvent
);
}
);
});
});
describe("forceAttribution", () => {
let setAttributionString;
beforeEach(() => {
setAttributionString = sandbox.spy(Router, "setAttributionString");
sandbox.stub(global.Services.env, "set");
});
afterEach(() => {
sandbox.reset();
});
it("should double encode on windows", async () => {
sandbox.stub(fakeAttributionCode, "writeAttributionFile");
Router.forceAttribution({ foo: "FOO!", eh: "NOPE", bar: "BAR?" });
assert.notCalled(setAttributionString);
assert.calledWithMatch(
fakeAttributionCode.writeAttributionFile,
"foo%3DFOO!%26bar%3DBAR%253F"
);
});
it("should set attribution string on mac", async () => {
sandbox.stub(global.AppConstants, "platform").value("macosx");
Router.forceAttribution({ foo: "FOO!", eh: "NOPE", bar: "BAR?" });
assert.calledOnce(setAttributionString);
assert.calledWithMatch(
setAttributionString,
"foo%3DFOO!%26bar%3DBAR%253F"
);
});
});
describe("#forceWNPanel", () => {
let browser = {
ownerGlobal: {
document: new Document(),
PanelUI: {
showSubView: sinon.stub(),
panel: {
setAttribute: sinon.stub(),
},
},
},
};
let fakePanel = {
setAttribute: sinon.stub(),
};
sinon
.stub(browser.ownerGlobal.document, "getElementById")
.returns(fakePanel);
it("should call enableToolbarButton", async () => {
await Router.forceWNPanel(browser);
assert.calledOnce(FakeToolbarPanelHub.enableToolbarButton);
assert.calledOnce(browser.ownerGlobal.PanelUI.showSubView);
assert.calledWith(fakePanel.setAttribute, "noautohide", true);
});
});
describe("_triggerHandler", () => {
it("should call #sendTriggerMessage with the correct trigger", () => {
const getter = sandbox.stub();
getter.returns(false);
sandbox.stub(global.BrowserHandler, "kiosk").get(getter);
sinon.spy(Router, "sendTriggerMessage");
const browser = {};
const trigger = { id: "FAKE_TRIGGER", param: "some fake param" };
Router._triggerHandler(browser, trigger);
assert.calledOnce(Router.sendTriggerMessage);
assert.calledWith(
Router.sendTriggerMessage,
sandbox.match({
id: "FAKE_TRIGGER",
param: "some fake param",
})
);
});
});
describe("_triggerHandler_kiosk", () => {
it("should not call #sendTriggerMessage", () => {
const getter = sandbox.stub();
getter.returns(true);
sandbox.stub(global.BrowserHandler, "kiosk").get(getter);
sinon.spy(Router, "sendTriggerMessage");
const browser = {};
const trigger = { id: "FAKE_TRIGGER", param: "some fake param" };
Router._triggerHandler(browser, trigger);
assert.notCalled(Router.sendTriggerMessage);
});
});
describe("valid preview endpoint", () => {
it("should report an error if url protocol is not https", () => {
sandbox.stub(console, "error");
assert.equal(false, Router._validPreviewEndpoint("http://foo.com"));
assert.calledTwice(console.error);
});
});
describe("impressions", () => {
describe("#addImpression for groups", () => {
it("should save an impression in each group-with-frequency in a message", async () => {
const fooMessageImpressions = [0];
const aGroupImpressions = [0, 1, 2];
const bGroupImpressions = [3, 4, 5];
const cGroupImpressions = [6, 7, 8];
const message = {
id: "foo",
provider: "bar",
groups: ["a", "b", "c"],
};
const groups = [
{ id: "a", frequency: { lifetime: 3 } },
{ id: "b", frequency: { lifetime: 4 } },
{ id: "c", frequency: { lifetime: 5 } },
];
await Router.setState(state => {
// Add provider
const providers = [...state.providers];
// Add fooMessageImpressions
// eslint-disable-next-line no-shadow
const messageImpressions = Object.assign(
{},
state.messageImpressions
);
let gImpressions = {};
gImpressions.a = aGroupImpressions;
gImpressions.b = bGroupImpressions;
gImpressions.c = cGroupImpressions;
messageImpressions.foo = fooMessageImpressions;
return {
providers,
messageImpressions,
groups,
groupImpressions: gImpressions,
};
});
await Router.addImpression(message);
assert.deepEqual(
Router.state.groupImpressions.a,
[0, 1, 2, 0],
"a impressions"
);
assert.deepEqual(
Router.state.groupImpressions.b,
[3, 4, 5, 0],
"b impressions"
);
assert.deepEqual(
Router.state.groupImpressions.c,
[6, 7, 8, 0],
"c impressions"
);
});
});
describe("#isBelowFrequencyCaps", () => {
it("should call #_isBelowItemFrequencyCap for the message and for the provider with the correct impressions and arguments", async () => {
sinon.spy(Router, "_isBelowItemFrequencyCap");
const MAX_MESSAGE_LIFETIME_CAP = 100; // Defined in ASRouter
const fooMessageImpressions = [0, 1];
const barGroupImpressions = [0, 1, 2];
const message = {
id: "foo",
provider: "bar",
groups: ["bar"],
frequency: { lifetime: 3 },
};
const groups = [{ id: "bar", frequency: { lifetime: 5 } }];
await Router.setState(state => {
// Add provider
const providers = [...state.providers];
// Add fooMessageImpressions
// eslint-disable-next-line no-shadow
const messageImpressions = Object.assign(
{},
state.messageImpressions
);
let gImpressions = {};
gImpressions.bar = barGroupImpressions;
messageImpressions.foo = fooMessageImpressions;
return {
providers,
messageImpressions,
groups,
groupImpressions: gImpressions,
};
});
await Router.isBelowFrequencyCaps(message);
assert.calledTwice(Router._isBelowItemFrequencyCap);
assert.calledWithExactly(
Router._isBelowItemFrequencyCap,
message,
fooMessageImpressions,
MAX_MESSAGE_LIFETIME_CAP
);
assert.calledWithExactly(
Router._isBelowItemFrequencyCap,
groups[0],
barGroupImpressions
);
});
});
describe("#_isBelowItemFrequencyCap", () => {
it("should return false if the # of impressions exceeds the maxLifetimeCap", () => {
const item = { id: "foo", frequency: { lifetime: 5 } };
const impressions = [0, 1];
const maxLifetimeCap = 1;
const result = Router._isBelowItemFrequencyCap(
item,
impressions,
maxLifetimeCap
);
assert.isFalse(result);
});
describe("lifetime frequency caps", () => {
it("should return true if .frequency is not defined on the item", () => {
const item = { id: "foo" };
const impressions = [0, 1];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isTrue(result);
});
it("should return true if there are no impressions", () => {
const item = {
id: "foo",
frequency: {
lifetime: 10,
custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],
},
};
const impressions = [];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isTrue(result);
});
it("should return true if the # of impressions is less than .frequency.lifetime of the item", () => {
const item = { id: "foo", frequency: { lifetime: 3 } };
const impressions = [0, 1];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isTrue(result);
});
it("should return false if the # of impressions is equal to .frequency.lifetime of the item", async () => {
const item = { id: "foo", frequency: { lifetime: 3 } };
const impressions = [0, 1, 2];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isFalse(result);
});
it("should return false if the # of impressions is greater than .frequency.lifetime of the item", async () => {
const item = { id: "foo", frequency: { lifetime: 3 } };
const impressions = [0, 1, 2, 3];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isFalse(result);
});
});
describe("custom frequency caps", () => {
it("should return true if impressions in the time period < the cap and total impressions < the lifetime cap", () => {
clock.tick(ONE_DAY_IN_MS + 10);
const item = {
id: "foo",
frequency: {
custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],
lifetime: 3,
},
};
const impressions = [0, ONE_DAY_IN_MS + 1];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isTrue(result);
});
it("should return false if impressions in the time period > the cap and total impressions < the lifetime cap", () => {
clock.tick(200);
const item = {
id: "msg1",
frequency: { custom: [{ period: 100, cap: 2 }], lifetime: 3 },
};
const impressions = [0, 160, 161];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isFalse(result);
});
it("should return false if impressions in one of the time periods > the cap and total impressions < the lifetime cap", () => {
clock.tick(ONE_DAY_IN_MS + 200);
const itemTrue = {
id: "msg2",
frequency: { custom: [{ period: 100, cap: 2 }] },
};
const itemFalse = {
id: "msg1",
frequency: {
custom: [
{ period: 100, cap: 2 },
{ period: ONE_DAY_IN_MS, cap: 3 },
],
},
};
const impressions = [
0,
ONE_DAY_IN_MS + 160,
ONE_DAY_IN_MS - 100,
ONE_DAY_IN_MS - 200,
];
assert.isTrue(Router._isBelowItemFrequencyCap(itemTrue, impressions));
assert.isFalse(
Router._isBelowItemFrequencyCap(itemFalse, impressions)
);
});
it("should return false if impressions in the time period < the cap and total impressions > the lifetime cap", () => {
clock.tick(ONE_DAY_IN_MS + 10);
const item = {
id: "msg1",
frequency: {
custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],
lifetime: 3,
},
};
const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isFalse(result);
});
it("should return true if daily impressions < the daily cap and there is no lifetime cap", () => {
clock.tick(ONE_DAY_IN_MS + 10);
const item = {
id: "msg1",
frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 2 }] },
};
const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isTrue(result);
});
it("should return false if daily impressions > the daily cap and there is no lifetime cap", () => {
clock.tick(ONE_DAY_IN_MS + 10);
const item = {
id: "msg1",
frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 2 }] },
};
const impressions = [
0,
1,
2,
3,
ONE_DAY_IN_MS + 1,
ONE_DAY_IN_MS + 2,
ONE_DAY_IN_MS + 3,
];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isFalse(result);
});
});
});
describe("#getLongestPeriod", () => {
it("should return the period if there is only one definition", () => {
const message = {
id: "foo",
frequency: { custom: [{ period: 200, cap: 2 }] },
};
assert.equal(Router.getLongestPeriod(message), 200);
});
it("should return the longest period if there are more than one definitions", () => {
const message = {
id: "foo",
frequency: {
custom: [
{ period: 1000, cap: 3 },
{ period: ONE_DAY_IN_MS, cap: 5 },
{ period: 100, cap: 2 },
],
},
};
assert.equal(Router.getLongestPeriod(message), ONE_DAY_IN_MS);
});
it("should return null if there are is no .frequency", () => {
const message = { id: "foo" };
assert.isNull(Router.getLongestPeriod(message));
});
it("should return null if there are is no .frequency.custom", () => {
const message = { id: "foo", frequency: { lifetime: 10 } };
assert.isNull(Router.getLongestPeriod(message));
});
});
describe("cleanup on init", () => {
it("should clear messageImpressions for messages which do not exist in state.messages", async () => {
const messages = [{ id: "foo", frequency: { lifetime: 10 } }];
messageImpressions = { foo: [0], bar: [0, 1] };
// Impressions for "bar" should be removed since that id does not exist in messages
const result = { foo: [0] };
await createRouterAndInit([
{ id: "onboarding", type: "local", messages, enabled: true },
]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
});
it("should clear messageImpressions older than the period if no lifetime impression cap is included", async () => {
const CURRENT_TIME = ONE_DAY_IN_MS * 2;
clock.tick(CURRENT_TIME);
const messages = [
{
id: "foo",
frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 5 }] },
},
];
messageImpressions = { foo: [0, 1, CURRENT_TIME - 10] };
// Only 0 and 1 are more than 24 hours before CURRENT_TIME
const result = { foo: [CURRENT_TIME - 10] };
await createRouterAndInit([
{ id: "onboarding", type: "local", messages, enabled: true },
]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
});
it("should clear messageImpressions older than the longest period if no lifetime impression cap is included", async () => {
const CURRENT_TIME = ONE_DAY_IN_MS * 2;
clock.tick(CURRENT_TIME);
const messages = [
{
id: "foo",
frequency: {
custom: [
{ period: ONE_DAY_IN_MS, cap: 5 },
{ period: 100, cap: 2 },
],
},
},
];
messageImpressions = { foo: [0, 1, CURRENT_TIME - 10] };
// Only 0 and 1 are more than 24 hours before CURRENT_TIME
const result = { foo: [CURRENT_TIME - 10] };
await createRouterAndInit([
{ id: "onboarding", type: "local", messages, enabled: true },
]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
});
it("should clear messageImpressions if they are not properly formatted", async () => {
const messages = [{ id: "foo", frequency: { lifetime: 10 } }];
// this is impromperly formatted since messageImpressions are supposed to be an array
messageImpressions = { foo: 0 };
const result = {};
await createRouterAndInit([
{ id: "onboarding", type: "local", messages, enabled: true },
]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
});
it("should not clear messageImpressions for messages which do exist in state.messages", async () => {
const messages = [
{ id: "foo", frequency: { lifetime: 10 } },
{ id: "bar", frequency: { lifetime: 10 } },
];
messageImpressions = { foo: [0], bar: [] };
await createRouterAndInit([
{ id: "onboarding", type: "local", messages, enabled: true },
]);
assert.notCalled(Router._storage.set);
assert.deepEqual(Router.state.messageImpressions, messageImpressions);
});
});
});
describe("#_onLocaleChanged", () => {
it("should call _maybeUpdateL10nAttachment in the handler", async () => {
sandbox.spy(Router, "_maybeUpdateL10nAttachment");
await Router._onLocaleChanged();
assert.calledOnce(Router._maybeUpdateL10nAttachment);
});
});
describe("#_maybeUpdateL10nAttachment", () => {
it("should update the l10n attachment if the locale was changed", async () => {
const getter = sandbox.stub();
getter.onFirstCall().returns("en-US");
getter.onSecondCall().returns("fr");
sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(getter);
const provider = {
id: "cfr",
enabled: true,
type: "remote-settings",
collection: "cfr",
};
await createRouterAndInit([provider]);
sandbox.spy(Router, "setState");
Router.loadMessagesFromAllProviders.resetHistory();
await Router._maybeUpdateL10nAttachment();
assert.calledWith(Router.setState, {
localeInUse: "fr",
providers: [
{
id: "cfr",
enabled: true,
type: "remote-settings",
collection: "cfr",
lastUpdated: undefined,
errors: [],
},
],
});
assert.calledOnce(Router.loadMessagesFromAllProviders);
});
it("should not update the l10n attachment if the provider doesn't need l10n attachment", async () => {
const getter = sandbox.stub();
getter.onFirstCall().returns("en-US");
getter.onSecondCall().returns("fr");
sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(getter);
const provider = {
id: "localProvider",
enabled: true,
type: "local",
};
await createRouterAndInit([provider]);
Router.loadMessagesFromAllProviders.resetHistory();
sandbox.spy(Router, "setState");
await Router._maybeUpdateL10nAttachment();
assert.notCalled(Router.setState);
assert.notCalled(Router.loadMessagesFromAllProviders);
});
});
describe("#observe", () => {
it("should reload l10n for CFRPageActions when the `USE_REMOTE_L10N_PREF` pref is changed", () => {
sandbox.spy(CFRPageActions, "reloadL10n");
Router.observe("", "", USE_REMOTE_L10N_PREF);
assert.calledOnce(CFRPageActions.reloadL10n);
});
it("should not react to other pref changes", () => {
sandbox.spy(CFRPageActions, "reloadL10n");
Router.observe("", "", "foo");
assert.notCalled(CFRPageActions.reloadL10n);
});
});
describe("#loadAllMessageGroups", () => {
it("should disable the group if the pref is false", async () => {
sandbox.stub(ASRouterPreferences, "getUserPreference").returns(false);
sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").resolves([
{
id: "provider-group",
enabled: true,
type: "remote",
userPreferences: ["cfrAddons"],
},
]);
await Router.setState({
providers: [
{
id: "message-groups",
enabled: true,
collection: "collection",
type: "remote-settings",
},
],
});
await Router.loadAllMessageGroups();
const group = Router.state.groups.find(g => g.id === "provider-group");
assert.ok(group);
assert.propertyVal(group, "enabled", false);
});
it("should enable the group if at least one pref is true", async () => {
sandbox
.stub(ASRouterPreferences, "getUserPreference")
.withArgs("cfrAddons")
.returns(false)
.withArgs("cfrFeatures")
.returns(true);
sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").resolves([
{
id: "provider-group",
enabled: true,
type: "remote",
userPreferences: ["cfrAddons", "cfrFeatures"],
},
]);
await Router.setState({
providers: [
{
id: "message-groups",
enabled: true,
collection: "collection",
type: "remote-settings",
},
],
});
await Router.loadAllMessageGroups();
const group = Router.state.groups.find(g => g.id === "provider-group");
assert.ok(group);
assert.propertyVal(group, "enabled", true);
});
it("should be keep the group disabled if disabled is true", async () => {
sandbox.stub(ASRouterPreferences, "getUserPreference").returns(true);
sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").resolves([
{
id: "provider-group",
enabled: false,
type: "remote",
userPreferences: ["cfrAddons"],
},
]);
await Router.setState({
providers: [
{
id: "message-groups",
enabled: true,
collection: "collection",
type: "remote-settings",
},
],
});
await Router.loadAllMessageGroups();
const group = Router.state.groups.find(g => g.id === "provider-group");
assert.ok(group);
assert.propertyVal(group, "enabled", false);
});
it("should keep local groups unchanged if provider doesn't require an update", async () => {
sandbox.stub(MessageLoaderUtils, "shouldProviderUpdate").returns(false);
sandbox.stub(MessageLoaderUtils, "_loadDataForProvider");
await Router.setState({
groups: [
{
id: "cfr",
enabled: true,
collection: "collection",
type: "remote-settings",
},
],
});
await Router.loadAllMessageGroups();
const group = Router.state.groups.find(g => g.id === "cfr");
assert.ok(group);
assert.propertyVal(group, "enabled", true);
// Because it should not have updated
assert.notCalled(MessageLoaderUtils._loadDataForProvider);
});
it("should update local groups on pref change (no RS update)", async () => {
sandbox.stub(MessageLoaderUtils, "shouldProviderUpdate").returns(false);
sandbox.stub(ASRouterPreferences, "getUserPreference").returns(false);
await Router.setState({
groups: [
{
id: "cfr",
enabled: true,
collection: "collection",
type: "remote-settings",
userPreferences: ["cfrAddons"],
},
],
});
await Router.loadAllMessageGroups();
const group = Router.state.groups.find(g => g.id === "cfr");
assert.ok(group);
// Pref changed, updated the group state
assert.propertyVal(group, "enabled", false);
});
});
describe("unblockAll", () => {
it("Clears the message block list and returns the state value", async () => {
await Router.setState({ messageBlockList: ["one", "two", "three"] });
assert.equal(Router.state.messageBlockList.length, 3);
const state = await Router.unblockAll();
assert.equal(Router.state.messageBlockList.length, 0);
assert.equal(state.messageBlockList.length, 0);
});
});
describe("#loadMessagesForProvider", () => {
it("should fetch messages from the ExperimentAPI", async () => {
const args = {
type: "remote-experiments",
featureIds: ["spotlight"],
};
await MessageLoaderUtils.loadMessagesForProvider(args);
assert.calledOnce(global.NimbusFeatures.spotlight.getAllVariables);
assert.calledOnce(global.ExperimentAPI.getExperimentMetaData);
assert.calledWithExactly(global.ExperimentAPI.getExperimentMetaData, {
featureId: "spotlight",
});
});
it("should handle the case of no experiments in the ExperimentAPI", async () => {
const args = {
type: "remote-experiments",
featureIds: ["infobar"],
};
global.ExperimentAPI.getExperiment.returns(null);
const result = await MessageLoaderUtils.loadMessagesForProvider(args);
assert.lengthOf(result.messages, 0);
});
it("should normally load ExperimentAPI messages", async () => {
const args = {
type: "remote-experiments",
featureIds: ["infobar"],
};
const enrollment = {
branch: {
slug: "branch01",
infobar: {
featureId: "infobar",
value: { id: "id01", trigger: { id: "openURL" } },
},
},
};
global.NimbusFeatures.infobar.getAllVariables.returns(
enrollment.branch.infobar.value
);
global.ExperimentAPI.getExperimentMetaData.returns({
branch: { slug: enrollment.branch.slug },
});
global.ExperimentAPI.getAllBranches.returns([
enrollment.branch,
{
slug: "control",
infobar: {
featureId: "infobar",
value: null,
},
},
]);
const result = await MessageLoaderUtils.loadMessagesForProvider(args);
assert.lengthOf(result.messages, 1);
});
it("should skip disabled features and not load the messages", async () => {
const args = {
type: "remote-experiments",
featureIds: ["cfr"],
};
global.NimbusFeatures.cfr.getAllVariables.returns(null);
const result = await MessageLoaderUtils.loadMessagesForProvider(args);
assert.lengthOf(result.messages, 0);
});
it("should fetch branches with trigger", async () => {
const args = {
type: "remote-experiments",
featureIds: ["cfr"],
};
const enrollment = {
slug: "exp01",
branch: {
slug: "branch01",
cfr: {
featureId: "cfr",
value: { id: "id01", trigger: { id: "openURL" } },
},
},
};
global.NimbusFeatures.cfr.getAllVariables.returns(
enrollment.branch.cfr.value
);
global.ExperimentAPI.getExperimentMetaData.returns({
slug: enrollment.slug,
active: true,
branch: { slug: enrollment.branch.slug },
});
global.ExperimentAPI.getAllBranches.resolves([
enrollment.branch,
{
slug: "branch02",
cfr: {
featureId: "cfr",
value: { id: "id02", trigger: { id: "openURL" } },
},
},
{
// This branch should not be loaded as it doesn't have the trigger
slug: "branch03",
cfr: {
featureId: "cfr",
value: { id: "id03" },
},
},
]);
const result = await MessageLoaderUtils.loadMessagesForProvider(args);
assert.equal(result.messages.length, 2);
assert.equal(result.messages[0].id, "id01");
assert.equal(result.messages[1].id, "id02");
assert.equal(result.messages[1].experimentSlug, "exp01");
assert.equal(result.messages[1].branchSlug, "branch02");
assert.deepEqual(result.messages[1].forReachEvent, {
sent: false,
group: "cfr",
});
});
it("should fetch branches with trigger even if enrolled branch is disabled", async () => {
const args = {
type: "remote-experiments",
featureIds: ["cfr"],
};
const enrollment = {
slug: "exp01",
branch: {
slug: "branch01",
cfr: {
featureId: "cfr",
value: {},
},
},
};
// Nedds to match the `featureIds` value to return an enrollment
// for that feature
global.NimbusFeatures.cfr.getAllVariables.returns(
enrollment.branch.cfr.value
);
global.ExperimentAPI.getExperimentMetaData.returns({
slug: enrollment.slug,
active: true,
branch: { slug: enrollment.branch.slug },
});
global.ExperimentAPI.getAllBranches.resolves([
enrollment.branch,
{
slug: "branch02",
cfr: {
featureId: "cfr",
value: { id: "id02", trigger: { id: "openURL" } },
},
},
{
// This branch should not be loaded as it doesn't have the trigger
slug: "branch03",
cfr: {
featureId: "cfr",
value: { id: "id03" },
},
},
]);
const result = await MessageLoaderUtils.loadMessagesForProvider(args);
assert.equal(result.messages.length, 1);
assert.equal(result.messages[0].id, "id02");
assert.equal(result.messages[0].experimentSlug, "exp01");
assert.equal(result.messages[0].branchSlug, "branch02");
assert.deepEqual(result.messages[0].forReachEvent, {
sent: false,
group: "cfr",
});
});
});
describe("#_remoteSettingsLoader", () => {
let provider;
let spy;
beforeEach(() => {
provider = {
id: "cfr",
collection: "cfr",
};
sandbox
.stub(MessageLoaderUtils, "_getRemoteSettingsMessages")
.resolves([{ id: "message_1" }]);
spy = sandbox.spy();
global.Downloader.prototype.downloadToDisk = spy;
});
it("should be called with the expected dir path", async () => {
const dlSpy = sandbox.spy(global, "Downloader");
sandbox
.stub(global.Services.locale, "appLocaleAsBCP47")
.get(() => "en-US");
await MessageLoaderUtils._remoteSettingsLoader(provider, {});
assert.calledWith(
dlSpy,
"main",
"ms-language-packs",
"browser",
"newtab"
);
});
it("should allow fetch for known locales", async () => {
sandbox
.stub(global.Services.locale, "appLocaleAsBCP47")
.get(() => "en-US");
await MessageLoaderUtils._remoteSettingsLoader(provider, {});
assert.calledOnce(spy);
});
it("should fallback to 'en-US' for locale 'und' ", async () => {
sandbox.stub(global.Services.locale, "appLocaleAsBCP47").get(() => "und");
const getRecordSpy = sandbox.spy(
global.KintoHttpClient.prototype,
"getRecord"
);
await MessageLoaderUtils._remoteSettingsLoader(provider, {});
assert.ok(getRecordSpy.args[0][0].includes("en-US"));
assert.calledOnce(spy);
});
it("should fallback to 'ja-JP-mac' for locale 'ja-JP-macos'", async () => {
sandbox
.stub(global.Services.locale, "appLocaleAsBCP47")
.get(() => "ja-JP-macos");
const getRecordSpy = sandbox.spy(
global.KintoHttpClient.prototype,
"getRecord"
);
await MessageLoaderUtils._remoteSettingsLoader(provider, {});
assert.ok(getRecordSpy.args[0][0].includes("ja-JP-mac"));
assert.calledOnce(spy);
});
it("should not allow fetch for unsupported locales", async () => {
sandbox
.stub(global.Services.locale, "appLocaleAsBCP47")
.get(() => "unkown");
await MessageLoaderUtils._remoteSettingsLoader(provider, {});
assert.notCalled(spy);
});
});
describe("#resetMessageState", () => {
it("should reset all message impressions", async () => {
await Router.setState({
messages: [{ id: "1" }, { id: "2" }],
});
await Router.setState({
messageImpressions: { 1: [0, 1, 2], 2: [0, 1, 2] },
}); // Add impressions for test messages
let impressions = Object.values(Router.state.messageImpressions);
assert.equal(impressions.filter(i => i.length).length, 2); // Both messages have impressions
Router.resetMessageState();
impressions = Object.values(Router.state.messageImpressions);
assert.isEmpty(impressions.filter(i => i.length)); // Both messages now have zero impressions
assert.calledWithExactly(Router._storage.set, "messageImpressions", {
1: [],
2: [],
});
});
});
describe("#resetGroupsState", () => {
it("should reset all group impressions", async () => {
await Router.setState({
groups: [{ id: "1" }, { id: "2" }],
});
await Router.setState({
groupImpressions: { 1: [0, 1, 2], 2: [0, 1, 2] },
}); // Add impressions for test groups
let impressions = Object.values(Router.state.groupImpressions);
assert.equal(impressions.filter(i => i.length).length, 2); // Both groups have impressions
Router.resetGroupsState();
impressions = Object.values(Router.state.groupImpressions);
assert.isEmpty(impressions.filter(i => i.length)); // Both groups now have zero impressions
assert.calledWithExactly(Router._storage.set, "groupImpressions", {
1: [],
2: [],
});
});
});
describe("#resetScreenImpressions", () => {
it("should reset all screen impressions", async () => {
await Router.setState({ screenImpressions: { 1: 1, 2: 2 } });
let impressions = Object.values(Router.state.screenImpressions);
assert.equal(impressions.filter(i => i).length, 2); // Both screens have impressions
Router.resetScreenImpressions();
impressions = Object.values(Router.state.screenImpressions);
assert.isEmpty(impressions.filter(i => i)); // Both screens now have zero impressions
assert.calledWithExactly(Router._storage.set, "screenImpressions", {});
});
});
describe("#editState", () => {
it("should update message impressions", async () => {
sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
await Router.setState({ messages: [{ id: "1" }, { id: "2" }] });
await Router.setState({
messageImpressions: { 1: [0, 1, 2], 2: [0, 1, 2] },
});
let impressions = Object.values(Router.state.messageImpressions);
assert.equal(impressions.filter(i => i.length).length, 2); // Both messages have impressions
Router.editState("messageImpressions", {
1: [],
2: [],
3: [0, 1, 2],
});
// The original messages now have zero impressions
assert.isEmpty(Router.state.messageImpressions["1"]);
assert.isEmpty(Router.state.messageImpressions["2"]);
// A new impression array was added for the new message
assert.equal(Router.state.messageImpressions["3"].length, 3);
assert.calledWithExactly(Router._storage.set, "messageImpressions", {
1: [],
2: [],
3: [0, 1, 2],
});
});
});
});