mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-08 12:19:05 +02:00
Differential Revision: https://phabricator.services.mozilla.com/D8443 --HG-- rename : browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.jsx => browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx rename : browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json => browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json rename : browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/_NewsletterSnippet.scss => browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/_SubmitFormSnippet.scss rename : browser/components/newtab/test/unit/asrouter/templates/NewsletterSnippet.test.jsx => browser/components/newtab/test/unit/asrouter/templates/SubmitFormSnippet.test.jsx extra : moz-landing-system : lando
1158 lines
56 KiB
JavaScript
1158 lines
56 KiB
JavaScript
import {_ASRouter, MessageLoaderUtils} from "lib/ASRouter.jsm";
|
|
import {
|
|
CHILD_TO_PARENT_MESSAGE_NAME,
|
|
FAKE_LOCAL_MESSAGES,
|
|
FAKE_LOCAL_PROVIDER,
|
|
FAKE_LOCAL_PROVIDERS,
|
|
FAKE_REMOTE_MESSAGES,
|
|
FAKE_REMOTE_PROVIDER,
|
|
FAKE_REMOTE_SETTINGS_PROVIDER,
|
|
FakeRemotePageManager,
|
|
PARENT_TO_CHILD_MESSAGE_NAME,
|
|
} from "./constants";
|
|
import {actionCreators as ac} from "common/Actions.jsm";
|
|
import {ASRouterPreferences} from "lib/ASRouterPreferences.jsm";
|
|
import {ASRouterTriggerListeners} from "lib/ASRouterTriggerListeners.jsm";
|
|
import {CFRPageActions} from "lib/CFRPageActions.jsm";
|
|
import {GlobalOverrider} from "test/unit/utils";
|
|
import ProviderResponseSchema from "content-src/asrouter/schemas/provider-response.schema.json";
|
|
import {QueryCache} from "lib/ASRouterTargeting.jsm";
|
|
|
|
const MESSAGE_PROVIDER_PREF_NAME = "browser.newtabpage.activity-stream.asrouter.messageProviders";
|
|
const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_PROVIDER, FAKE_REMOTE_SETTINGS_PROVIDER];
|
|
const ALL_MESSAGE_IDS = [...FAKE_LOCAL_MESSAGES, ...FAKE_REMOTE_MESSAGES].map(message => message.id);
|
|
const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
|
|
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
|
|
|
|
// Creates a message object that looks like messages returned by
|
|
// RemotePageManager listeners
|
|
function fakeAsyncMessage(action) {
|
|
return {data: action, target: new FakeRemotePageManager()};
|
|
}
|
|
// Create a message that looks like a user action
|
|
function fakeExecuteUserAction(action) {
|
|
return fakeAsyncMessage({data: action, type: "USER_ACTION"});
|
|
}
|
|
|
|
describe("ASRouter", () => {
|
|
let Router;
|
|
let channel;
|
|
let sandbox;
|
|
let messageBlockList;
|
|
let providerBlockList;
|
|
let messageImpressions;
|
|
let providerImpressions;
|
|
let previousSessionEnd;
|
|
let fetchStub;
|
|
let clock;
|
|
let getStringPrefStub;
|
|
let dispatchStub;
|
|
|
|
function createFakeStorage() {
|
|
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("providerImpressions").returns(Promise.resolve(providerImpressions));
|
|
getStub.withArgs("previousSessionEnd").returns(Promise.resolve(previousSessionEnd));
|
|
return {
|
|
get: getStub,
|
|
set: sandbox.stub().returns(Promise.resolve()),
|
|
};
|
|
}
|
|
|
|
function setMessageProviderPref(value) {
|
|
sandbox.stub(ASRouterPreferences, "providers").get(() => value);
|
|
}
|
|
|
|
async function createRouterAndInit(providers = FAKE_PROVIDERS) {
|
|
setMessageProviderPref(providers);
|
|
channel = new FakeRemotePageManager();
|
|
dispatchStub = sandbox.stub();
|
|
Router = new _ASRouter(FAKE_LOCAL_PROVIDERS);
|
|
await Router.init(channel, createFakeStorage(), dispatchStub);
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
messageBlockList = [];
|
|
providerBlockList = [];
|
|
messageImpressions = {};
|
|
providerImpressions = {};
|
|
previousSessionEnd = 100;
|
|
sandbox = sinon.sandbox.create();
|
|
|
|
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})});
|
|
getStringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref");
|
|
await createRouterAndInit();
|
|
});
|
|
afterEach(() => {
|
|
ASRouterPreferences.uninit();
|
|
sandbox.restore();
|
|
});
|
|
|
|
describe(".state", () => {
|
|
it("should throw if an attempt to set .state was made", () => {
|
|
assert.throws(() => {
|
|
Router.state = {};
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("#init", () => {
|
|
it("should add a message listener to the RemotePageManager for incoming messages", () => {
|
|
assert.calledWith(channel.addMessageListener, CHILD_TO_PARENT_MESSAGE_NAME);
|
|
const [, listenerAdded] = channel.addMessageListener.firstCall.args;
|
|
assert.isFunction(listenerAdded);
|
|
});
|
|
it("should set state.messageBlockList to the block list in persistent storage", async () => {
|
|
messageBlockList = ["foo"];
|
|
Router = new _ASRouter();
|
|
await Router.init(channel, createFakeStorage(), dispatchStub);
|
|
|
|
assert.deepEqual(Router.state.messageBlockList, ["foo"]);
|
|
});
|
|
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 Router.init(channel, createFakeStorage(), dispatchStub);
|
|
|
|
assert.deepEqual(Router.state.messageImpressions, messageImpressions);
|
|
});
|
|
it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
|
|
Router = new _ASRouter(FAKE_LOCAL_PROVIDERS);
|
|
|
|
const loadMessagesSpy = sandbox.spy(Router, "loadMessagesFromAllProviders");
|
|
await Router.init(channel, createFakeStorage(), dispatchStub);
|
|
|
|
assert.calledOnce(loadMessagesSpy);
|
|
assert.isArray(Router.state.messages);
|
|
assert.lengthOf(Router.state.messages, FAKE_LOCAL_MESSAGES.length + FAKE_REMOTE_MESSAGES.length);
|
|
});
|
|
it("should load additional whitelisted hosts", async () => {
|
|
getStringPrefStub.returns("[\"whitelist.com\"]");
|
|
await createRouterAndInit();
|
|
|
|
assert.propertyVal(Router.WHITELIST_HOSTS, "whitelist.com", "preview");
|
|
// Should still include the defaults
|
|
assert.lengthOf(Object.keys(Router.WHITELIST_HOSTS), 3);
|
|
});
|
|
it("should fallback to defaults if pref parsing fails", async () => {
|
|
getStringPrefStub.returns("err");
|
|
await createRouterAndInit();
|
|
|
|
assert.lengthOf(Object.keys(Router.WHITELIST_HOSTS), 2);
|
|
assert.propertyVal(Router.WHITELIST_HOSTS, "snippets-admin.mozilla.org", "preview");
|
|
assert.propertyVal(Router.WHITELIST_HOSTS, "activity-stream-icons.services.mozilla.com", "production");
|
|
});
|
|
it("should set this.dispatchToAS to the third parameter passed to .init()", async () => {
|
|
assert.equal(Router.dispatchToAS, dispatchStub);
|
|
});
|
|
it("should set state.previousSessionEnd from IndexedDB", async () => {
|
|
previousSessionEnd = 200;
|
|
await createRouterAndInit();
|
|
|
|
assert.equal(Router.state.previousSessionEnd, previousSessionEnd);
|
|
});
|
|
it("should dispatch a AS_ROUTER_INITIALIZED event to AS with ASRouterPreferences.specialConditions", async () => {
|
|
assert.calledOnce(Router.dispatchToAS);
|
|
assert.calledWith(Router.dispatchToAS, ac.BroadcastToContent({type: "AS_ROUTER_INITIALIZED", data: ASRouterPreferences.specialConditions}));
|
|
});
|
|
});
|
|
|
|
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 call loadMessagesFromAllProviders on pref change", () => {
|
|
sandbox.spy(Router, "loadMessagesFromAllProviders");
|
|
|
|
ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);
|
|
|
|
assert.calledOnce(Router.loadMessagesFromAllProviders);
|
|
});
|
|
it("should update the list of providers on pref change", () => {
|
|
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);
|
|
|
|
const provider = Router.state.providers.find(p => p.url === "baz.com");
|
|
assert.lengthOf(Router.state.providers, length);
|
|
assert.isDefined(provider);
|
|
});
|
|
});
|
|
|
|
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);
|
|
await Router.setState({foo: 123});
|
|
|
|
assert.calledWith(channel.sendAsyncMessage, "ASRouter:parent-to-child", {type: "ADMIN_SET_STATE", data: Router.state});
|
|
});
|
|
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(channel.sendAsyncMessage);
|
|
});
|
|
});
|
|
|
|
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.equal(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;
|
|
|
|
clock.tick(300);
|
|
|
|
await Router.loadMessagesFromAllProviders();
|
|
assert.equal(Router.state, previousState);
|
|
});
|
|
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})});
|
|
|
|
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-curly-newline */ /* eslint-disable object-property-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-curly-newline */ /* eslint-enable object-property-newline */
|
|
|
|
assert.calledTwice(ASRouterTriggerListeners.get("openURL").init);
|
|
assert.calledWithExactly(ASRouterTriggerListeners.get("openURL").init,
|
|
Router._triggerHandler, ["www.mozilla.org", "www.mozilla.com"]);
|
|
assert.calledWithExactly(ASRouterTriggerListeners.get("openURL").init,
|
|
Router._triggerHandler, ["www.example.com"]);
|
|
});
|
|
it("should gracefully handle RemoteSettings blowing up", async () => {
|
|
sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").rejects("fake error");
|
|
await createRouterAndInit();
|
|
});
|
|
});
|
|
|
|
describe("#_updateMessageProviders", () => {
|
|
it("should correctly replace %STARTPAGE_VERSION% in remote provider urls", () => {
|
|
// 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]);
|
|
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", () => {
|
|
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]);
|
|
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", () => {
|
|
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);
|
|
Router._updateMessageProviders();
|
|
assert.equal(Router.state.providers.length, 1);
|
|
assert.equal(Router.state.providers[0].id, providers[1].id);
|
|
});
|
|
});
|
|
|
|
describe("blocking", () => {
|
|
it("should not return a blocked message", async () => {
|
|
// Block all messages except the first
|
|
await Router.setState(() => ({messageBlockList: ALL_MESSAGE_IDS.slice(1)}));
|
|
const targetStub = {sendAsyncMessage: sandbox.stub()};
|
|
|
|
await Router.sendNextMessage(targetStub);
|
|
|
|
assert.calledOnce(targetStub.sendAsyncMessage);
|
|
assert.equal(Router.state.lastMessageId, ALL_MESSAGE_IDS[0]);
|
|
});
|
|
it("should not return a message from a blocked 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(() => ({providerBlockList: [FAKE_LOCAL_PROVIDER.id]}));
|
|
const targetStub = {sendAsyncMessage: sandbox.stub()};
|
|
|
|
await Router.sendNextMessage(targetStub);
|
|
|
|
assert.calledOnce(targetStub.sendAsyncMessage);
|
|
assert.equal(Router.state.lastMessageId, FAKE_REMOTE_MESSAGES[0].id);
|
|
});
|
|
it("should not return a message if all messages are blocked", async () => {
|
|
await Router.setState(() => ({messageBlockList: ALL_MESSAGE_IDS}));
|
|
const targetStub = {sendAsyncMessage: sandbox.stub()};
|
|
|
|
await Router.sendNextMessage(targetStub);
|
|
|
|
assert.calledOnce(targetStub.sendAsyncMessage);
|
|
assert.equal(Router.state.lastMessageId, null);
|
|
});
|
|
});
|
|
|
|
describe("#uninit", () => {
|
|
it("should remove the message listener on the RemotePageManager", () => {
|
|
const [, listenerAdded] = channel.addMessageListener.firstCall.args;
|
|
assert.isFunction(listenerAdded);
|
|
|
|
Router.uninit();
|
|
|
|
assert.calledWith(channel.removeMessageListener, CHILD_TO_PARENT_MESSAGE_NAME, listenerAdded);
|
|
});
|
|
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 .dispatchToAS to null", () => {
|
|
Router.uninit();
|
|
assert.isNull(Router.dispatchToAS);
|
|
});
|
|
it("should save previousSessionEnd", () => {
|
|
Router.uninit();
|
|
|
|
assert.calledOnce(Router._storage.set);
|
|
assert.calledWithExactly(Router._storage.set, "previousSessionEnd", sinon.match.number);
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: SNIPPETS_REQUEST", () => {
|
|
it("should set state.lastMessageId to a message id", async () => {
|
|
await Router.onMessage(fakeAsyncMessage({type: "SNIPPETS_REQUEST"}));
|
|
|
|
assert.include(ALL_MESSAGE_IDS, Router.state.lastMessageId);
|
|
});
|
|
it("should send a message back to the to the target", async () => {
|
|
// force the only message to be a regular message so getRandomItemFromArray picks it
|
|
await Router.setState({messages: [{id: "foo", template: "simple_template", content: {title: "Foo", body: "Foo123"}}]});
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
|
|
await Router.onMessage(msg);
|
|
const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
|
|
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: currentMessage});
|
|
});
|
|
it("should send a message back to the to the target if there is a bundle, too", async () => {
|
|
// force the only message to be a bundled message so getRandomItemFromArray picks it
|
|
sandbox.stub(Router, "_findProvider").returns(null);
|
|
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 1, content: {title: "Foo1", body: "Foo123-1"}}]});
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
|
|
await Router.onMessage(msg);
|
|
const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
|
|
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
|
|
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
|
|
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, currentMessage.content);
|
|
});
|
|
it("should properly order the message's bundle if specified", async () => {
|
|
// force the only messages to be a bundled messages so getRandomItemFromArray picks one of them
|
|
sandbox.stub(Router, "_findProvider").returns(null);
|
|
const firstMessage = {id: "foo2", template: "simple_template", bundled: 2, order: 1, content: {title: "Foo2", body: "Foo123-2"}};
|
|
const secondMessage = {id: "foo1", template: "simple_template", bundled: 2, order: 2, content: {title: "Foo1", body: "Foo123-1"}};
|
|
await Router.setState({messages: [secondMessage, firstMessage]});
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
|
|
await Router.onMessage(msg);
|
|
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
|
|
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
|
|
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, firstMessage.content);
|
|
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[1].content, secondMessage.content);
|
|
});
|
|
it("should return a null bundle if we do not have enough messages to fill the bundle", async () => {
|
|
// force the only message to be a bundled message that needs 2 messages in the bundle
|
|
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
|
|
const bundle = await Router._getBundledMessages(Router.state.messages[0]);
|
|
assert.equal(bundle, null);
|
|
});
|
|
it("should send down extra attributes in the bundle if they exist", async () => {
|
|
sandbox.stub(Router, "_findProvider").returns({getExtraAttributes() { return Promise.resolve({header: "header"}); }});
|
|
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 1, content: {title: "Foo1", body: "Foo123-1"}}]});
|
|
const result = await Router._getBundledMessages(Router.state.messages[0]);
|
|
assert.equal(result.extraTemplateStrings.header, "header");
|
|
});
|
|
it("should send a CLEAR_ALL message if no bundle available", async () => {
|
|
// force the only message to be a bundled message that needs 2 messages in the bundle
|
|
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
|
|
await Router.onMessage(msg);
|
|
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
|
|
});
|
|
it("should send a CLEAR_ALL message if no messages are available", async () => {
|
|
await Router.setState({messages: []});
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
|
|
});
|
|
it("should make a request to the provided endpoint on SNIPPETS_REQUEST", async () => {
|
|
const url = "https://snippets-admin.mozilla.org/foo";
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledWith(global.fetch, url);
|
|
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
|
|
});
|
|
it("should make a request to the provided endpoint on ADMIN_CONNECT_STATE and remove the endpoint", async () => {
|
|
const url = "https://snippets-admin.mozilla.org/foo";
|
|
const msg = fakeAsyncMessage({type: "ADMIN_CONNECT_STATE", data: {endpoint: {url}}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledWith(global.fetch, url);
|
|
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
|
|
});
|
|
it("should dispatch SNIPPETS_PREVIEW_MODE when adding a preview endpoint", async () => {
|
|
const url = "https://snippets-admin.mozilla.org/foo";
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledWithExactly(Router.dispatchToAS, ac.OnlyToOneContent({type: "SNIPPETS_PREVIEW_MODE"}, msg.target.portID));
|
|
});
|
|
it("should not add a url that is not from a whitelisted host", async () => {
|
|
const url = "https://mozilla.org";
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
|
|
});
|
|
it("should reject bad urls", async () => {
|
|
const url = "foo";
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: BLOCK_MESSAGE_BY_ID", () => {
|
|
it("should add the id to the messageBlockList and broadcast a CLEAR_MESSAGE message with the id", async () => {
|
|
await Router.setState({lastMessageId: "foo"});
|
|
const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.isTrue(Router.state.messageBlockList.includes("foo"));
|
|
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: "foo"}});
|
|
});
|
|
it("should not broadcast CLEAR_MESSAGE if preventDismiss is true", async () => {
|
|
const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo", preventDismiss: true}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.notCalled(channel.sendAsyncMessage);
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: DISMISS_MESSAGE_BY_ID", () => {
|
|
it("should reply with CLEAR_MESSAGE with the correct id", async () => {
|
|
const msg = fakeAsyncMessage({type: "DISMISS_MESSAGE_BY_ID", data: {id: "foo"}});
|
|
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: "foo"}});
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: BLOCK_PROVIDER_BY_ID", () => {
|
|
it("should add the provider id to the providerBlockList and broadcast a CLEAR_PROVIDER with the provider id", async () => {
|
|
const msg = fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "bar"}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.isTrue(Router.state.providerBlockList.includes("bar"));
|
|
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_PROVIDER", data: {id: "bar"}});
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: BLOCK_BUNDLE", () => {
|
|
it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
|
|
const bundleIds = [FAKE_BUNDLE[0].id, FAKE_BUNDLE[1].id];
|
|
await Router.setState({lastMessageId: "foo"});
|
|
const msg = fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
|
|
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
|
|
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
|
|
assert.calledWithExactly(Router._storage.set, "messageBlockList", bundleIds);
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
|
|
it("should remove the id from the messageBlockList", async () => {
|
|
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
|
|
assert.isTrue(Router.state.messageBlockList.includes("foo"));
|
|
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
|
|
|
|
assert.isFalse(Router.state.messageBlockList.includes("foo"));
|
|
});
|
|
it("should save the messageBlockList", async () => {
|
|
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
|
|
|
|
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: UNBLOCK_PROVIDER_BY_ID", () => {
|
|
it("should remove the id from the providerBlockList", async () => {
|
|
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
|
|
assert.isTrue(Router.state.providerBlockList.includes("foo"));
|
|
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
|
|
|
|
assert.isFalse(Router.state.providerBlockList.includes("foo"));
|
|
});
|
|
it("should save the providerBlockList", async () => {
|
|
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
|
|
|
|
assert.calledWithExactly(Router._storage.set, "providerBlockList", []);
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: UNBLOCK_BUNDLE", () => {
|
|
it("should remove all the ids in the bundle from the messageBlockList", async () => {
|
|
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
|
|
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
|
|
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
|
|
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
|
|
|
|
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
|
|
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
|
|
});
|
|
it("should save the messageBlockList", async () => {
|
|
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
|
|
|
|
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: ADMIN_CONNECT_STATE", () => {
|
|
it("should send a message containing the whole state", async () => {
|
|
const msg = fakeAsyncMessage({type: "ADMIN_CONNECT_STATE"});
|
|
await Router.onMessage(msg);
|
|
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: Router.state});
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: SNIPPETS_REQUEST", () => {
|
|
it("should call sendNextMessage on SNIPPETS_REQUEST", async () => {
|
|
sandbox.stub(Router, "sendNextMessage").resolves();
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
|
|
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(Router.sendNextMessage);
|
|
assert.calledWithExactly(Router.sendNextMessage, sinon.match.instanceOf(FakeRemotePageManager), {});
|
|
});
|
|
it("should return the preview message if that's available and remove it from Router.state", async () => {
|
|
const expectedObj = {provider: "preview"};
|
|
Router.setState({messages: [expectedObj]});
|
|
|
|
await Router.sendNextMessage(channel);
|
|
|
|
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: expectedObj});
|
|
assert.isUndefined(Router.state.messages.find(m => m.provider === "preview"));
|
|
});
|
|
it("should call _getBundledMessages if we request a message that needs to be bundled", async () => {
|
|
sandbox.stub(Router, "_getBundledMessages").resolves();
|
|
// forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
|
|
const [, testMessage] = Router.state.messages;
|
|
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(Router._getBundledMessages);
|
|
});
|
|
it("should properly pick another message of the same template if it is bundled; force = true", async () => {
|
|
// forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
|
|
const [, testMessage1, testMessage2] = Router.state.messages;
|
|
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage1.id}});
|
|
await Router.onMessage(msg);
|
|
|
|
// Expected object should have some properties of the original message it picked (testMessage1)
|
|
// plus the bundled content of the others that it picked of the same template (testMessage2)
|
|
const expectedObj = {
|
|
template: testMessage1.template,
|
|
provider: testMessage1.provider,
|
|
bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id}],
|
|
};
|
|
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
|
|
});
|
|
it("should properly pick another message of the same template if it is bundled; force = false", async () => {
|
|
// forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
|
|
const [, testMessage1, testMessage2] = Router.state.messages;
|
|
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage1.id}});
|
|
await Router.setMessageById(testMessage1.id, msg.target, false);
|
|
|
|
// Expected object should have some properties of the original message it picked (testMessage1)
|
|
// plus the bundled content of the others that it picked of the same template (testMessage2)
|
|
const expectedObj = {
|
|
template: testMessage1.template,
|
|
provider: testMessage1.provider,
|
|
bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id, order: 2}],
|
|
};
|
|
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
|
|
});
|
|
it("should get the bundle and send the message if the message has a bundle", async () => {
|
|
sandbox.stub(Router, "sendNextMessage").resolves();
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
|
|
msg.bundled = 2; // force this message to want to be bundled
|
|
await Router.onMessage(msg);
|
|
assert.calledOnce(Router.sendNextMessage);
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: TRIGGER", () => {
|
|
it("should pass the trigger to ASRouterTargeting on TRIGGER message", async () => {
|
|
sandbox.stub(Router, "_findMessage").resolves();
|
|
const msg = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(Router._findMessage);
|
|
assert.deepEqual(Router._findMessage.firstCall.args[1], {id: "firstRun"});
|
|
});
|
|
it("consider the trigger when picking a message", async () => {
|
|
const messages = [
|
|
{id: "foo1", template: "simple_template", bundled: 1, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}},
|
|
];
|
|
|
|
const {data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
|
|
const message = await Router._findMessage(messages, data.data.trigger);
|
|
assert.equal(message, messages[0]);
|
|
});
|
|
it("should pick a message with the right targeting and trigger", async () => {
|
|
let messages = [
|
|
{id: "foo1", template: "simple_template", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}},
|
|
{id: "foo2", template: "simple_template", bundled: 2, trigger: {id: "bar"}, content: {title: "Foo2", body: "Foo123-2"}},
|
|
{id: "foo3", template: "simple_template", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo3", body: "Foo123-3"}},
|
|
];
|
|
sandbox.stub(Router, "_findProvider").returns(null);
|
|
await Router.setState({messages});
|
|
const {target} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
|
|
let {bundle} = await Router._getBundledMessages(messages[0], target, {id: "foo"});
|
|
assert.equal(bundle.length, 2);
|
|
// it should have picked foo1 and foo3 only
|
|
assert.isTrue(bundle.every(elem => elem.id === "foo1" || elem.id === "foo3"));
|
|
});
|
|
it("should have previousSessionEnd in the message context", () => {
|
|
assert.propertyVal(Router._getMessagesContext(), "previousSessionEnd", 100);
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: OVERRIDE_MESSAGE", () => {
|
|
it("should broadcast a SET_MESSAGE message to all clients with a particular id", async () => {
|
|
const [testMessage] = Router.state.messages;
|
|
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: testMessage});
|
|
});
|
|
|
|
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]});
|
|
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.notCalled(msg.target.sendAsyncMessage);
|
|
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]});
|
|
await Router._sendMessageToTarget(testMessage, {}, {}, false);
|
|
|
|
assert.calledOnce(CFRPageActions.addRecommendation);
|
|
});
|
|
|
|
it("should broadcast CLEAR_ALL if provided id did not resolve to a message", async () => {
|
|
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: -1}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: Onboarding actions", () => {
|
|
it("should call OpenBrowserWindow with a private window on OPEN_PRIVATE_BROWSER_WINDOW", async () => {
|
|
let [testMessage] = Router.state.messages;
|
|
const msg = fakeExecuteUserAction({type: "OPEN_PRIVATE_BROWSER_WINDOW", data: testMessage});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledWith(msg.target.browser.ownerGlobal.OpenBrowserWindow, {private: true});
|
|
});
|
|
it("should call openLinkIn with the correct params on OPEN_URL", async () => {
|
|
let [testMessage] = Router.state.messages;
|
|
testMessage.button_action = {type: "OPEN_URL", data: {args: "some/url.com"}};
|
|
const msg = fakeExecuteUserAction(testMessage.button_action);
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
|
|
assert.calledWith(msg.target.browser.ownerGlobal.openLinkIn,
|
|
"some/url.com", "tabshifted", {"private": false, "triggeringPrincipal": undefined});
|
|
});
|
|
it("should call openLinkIn with the correct params on OPEN_ABOUT_PAGE", async () => {
|
|
let [testMessage] = Router.state.messages;
|
|
testMessage.button_action = {type: "OPEN_ABOUT_PAGE", data: {args: "something"}};
|
|
const msg = fakeExecuteUserAction(testMessage.button_action);
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(msg.target.browser.ownerGlobal.openTrustedLinkIn);
|
|
assert.calledWith(msg.target.browser.ownerGlobal.openTrustedLinkIn, "about:something", "tab");
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: SHOW_FIREFOX_ACCOUNTS", () => {
|
|
let globals;
|
|
beforeEach(() => {
|
|
globals = new GlobalOverrider();
|
|
globals.set("FxAccounts", {config: {promiseSignUpURI: sandbox.stub().resolves("some/url")}});
|
|
});
|
|
it("should call openLinkIn with the correct params on OPEN_URL", async () => {
|
|
let [testMessage] = Router.state.messages;
|
|
testMessage.button_action = {type: "SHOW_FIREFOX_ACCOUNTS"};
|
|
const msg = fakeExecuteUserAction(testMessage.button_action);
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
|
|
assert.calledWith(msg.target.browser.ownerGlobal.openLinkIn,
|
|
"some/url", "tabshifted", {"private": false, "triggeringPrincipal": undefined});
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: INSTALL_ADDON_FROM_URL", () => {
|
|
it("should call installAddonFromURL with correct arguments", async () => {
|
|
sandbox.stub(MessageLoaderUtils, "installAddonFromURL").resolves(null);
|
|
const msg = fakeExecuteUserAction({type: "INSTALL_ADDON_FROM_URL", data: {url: "foo.com"}});
|
|
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(MessageLoaderUtils.installAddonFromURL);
|
|
assert.calledWithExactly(MessageLoaderUtils.installAddonFromURL, msg.target.browser, "foo.com");
|
|
});
|
|
});
|
|
|
|
describe("#dispatch(action, target)", () => {
|
|
it("should an action and target to onMessage", async () => {
|
|
// use the IMPRESSION action to make sure actions are actually getting processed
|
|
sandbox.stub(Router, "addImpression");
|
|
sandbox.spy(Router, "onMessage");
|
|
const target = {};
|
|
const action = {type: "IMPRESSION"};
|
|
|
|
Router.dispatch(action, target);
|
|
|
|
assert.calledWith(Router.onMessage, {data: action, target});
|
|
assert.calledOnce(Router.addImpression);
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: DOORHANGER_TELEMETRY", () => {
|
|
it("should dispatch an AS_ROUTER_TELEMETRY_USER_EVENT on DOORHANGER_TELEMETRY message", async () => {
|
|
const msg = fakeAsyncMessage({type: "DOORHANGER_TELEMETRY", data: {message_id: "foo"}});
|
|
dispatchStub.reset();
|
|
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(dispatchStub);
|
|
const [action] = dispatchStub.firstCall.args;
|
|
assert.equal(action.type, "AS_ROUTER_TELEMETRY_USER_EVENT");
|
|
assert.equal(action.data.message_id, "foo");
|
|
});
|
|
});
|
|
|
|
describe("#onMessage: EXPIRE_QUERY_CACHE", () => {
|
|
it("should clear all QueryCache getters", async () => {
|
|
const msg = fakeAsyncMessage({type: "EXPIRE_QUERY_CACHE"});
|
|
sandbox.stub(QueryCache, "expireAll");
|
|
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(QueryCache.expireAll);
|
|
});
|
|
});
|
|
|
|
describe("_triggerHandler", () => {
|
|
it("should call #onMessage with the correct trigger", () => {
|
|
sinon.spy(Router, "onMessage");
|
|
const target = {};
|
|
const trigger = {id: "FAKE_TRIGGER", param: "some fake param"};
|
|
Router._triggerHandler(target, trigger);
|
|
assert.calledOnce(Router.onMessage);
|
|
assert.calledWithExactly(Router.onMessage, {target, data: {type: "TRIGGER", data: {trigger}}});
|
|
});
|
|
});
|
|
|
|
describe("#UITour", () => {
|
|
let globals;
|
|
let showMenuStub;
|
|
beforeEach(() => {
|
|
globals = new GlobalOverrider();
|
|
showMenuStub = sandbox.stub();
|
|
globals.set("UITour", {showMenu: showMenuStub});
|
|
});
|
|
it("should call UITour.showMenu with the correct params on OPEN_APPLICATIONS_MENU", async () => {
|
|
const msg = fakeExecuteUserAction({type: "OPEN_APPLICATIONS_MENU", data: {args: "appMenu"}});
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(showMenuStub);
|
|
assert.calledWith(showMenuStub, msg.target.browser.ownerGlobal, "appMenu");
|
|
});
|
|
});
|
|
|
|
describe("valid preview endpoint", () => {
|
|
it("should report an error if url protocol is not https", () => {
|
|
sandbox.stub(Cu, "reportError");
|
|
|
|
assert.equal(false, Router._validPreviewEndpoint("http://foo.com"));
|
|
assert.calledTwice(Cu.reportError);
|
|
});
|
|
});
|
|
|
|
describe("impressions", () => {
|
|
async function addProviderWithFrequency(id, frequency) {
|
|
await Router.setState(state => {
|
|
const newProvider = {id, frequency};
|
|
const providers = [...state.providers, newProvider];
|
|
return {providers};
|
|
});
|
|
}
|
|
|
|
describe("frequency normalisation", () => {
|
|
beforeEach(async () => {
|
|
const messages = [{frequency: {custom: [{period: "daily", cap: 10}]}}];
|
|
const provider = {id: "foo", frequency: {custom: [{period: "daily", cap: 100}]}, messages, enabled: true};
|
|
await createRouterAndInit([provider]);
|
|
});
|
|
|
|
it("period aliases in provider frequency caps should be normalised", () => {
|
|
const [provider] = Router.state.providers;
|
|
assert.equal(provider.frequency.custom[0].period, ONE_DAY_IN_MS);
|
|
});
|
|
it("period aliases in message frequency caps should be normalised", async () => {
|
|
const [message] = Router.state.messages;
|
|
assert.equal(message.frequency.custom[0].period, ONE_DAY_IN_MS);
|
|
});
|
|
});
|
|
|
|
describe("#addImpression", () => {
|
|
it("should add a message impression and update _storage with the current time if the message has frequency caps", async () => {
|
|
clock.tick(42);
|
|
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo", provider: FAKE_LOCAL_PROVIDER.id, frequency: {lifetime: 5}}});
|
|
await Router.onMessage(msg);
|
|
assert.isArray(Router.state.messageImpressions.foo);
|
|
assert.deepEqual(Router.state.messageImpressions.foo, [42]);
|
|
assert.calledWith(Router._storage.set, "messageImpressions", {foo: [42]});
|
|
});
|
|
it("should not add a message impression if the message doesn't have frequency caps", async () => {
|
|
// Note that storage.set is called during initialization, so it needs to be reset
|
|
Router._storage.set.reset();
|
|
clock.tick(42);
|
|
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo"}});
|
|
await Router.onMessage(msg);
|
|
assert.notProperty(Router.state.messageImpressions, "foo");
|
|
assert.notCalled(Router._storage.set);
|
|
});
|
|
it("should add a provider impression and update _storage with the current time if the message's provider has frequency caps", async () => {
|
|
clock.tick(42);
|
|
await addProviderWithFrequency("foo", {lifetime: 5});
|
|
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "bar", provider: "foo"}});
|
|
await Router.onMessage(msg);
|
|
assert.isArray(Router.state.providerImpressions.foo);
|
|
assert.deepEqual(Router.state.providerImpressions.foo, [42]);
|
|
assert.calledWith(Router._storage.set, "providerImpressions", {foo: [42]});
|
|
});
|
|
it("should not add a provider impression if the message's provider doesn't have frequency caps", async () => {
|
|
// Note that storage.set is called during initialization, so it needs to be reset
|
|
Router._storage.set.reset();
|
|
clock.tick(42);
|
|
// Add "foo" provider with no frequency
|
|
await addProviderWithFrequency("foo", null);
|
|
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "bar", provider: "foo"}});
|
|
await Router.onMessage(msg);
|
|
assert.notProperty(Router.state.providerImpressions, "foo");
|
|
assert.notCalled(Router._storage.set);
|
|
});
|
|
});
|
|
|
|
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 barProviderImpressions = [0, 1, 2];
|
|
|
|
const message = {id: "foo", provider: "bar", frequency: {lifetime: 3}};
|
|
const provider = {id: "bar", frequency: {lifetime: 5}};
|
|
|
|
await Router.setState(state => {
|
|
// Add provider
|
|
const providers = [...state.providers, provider];
|
|
// Add fooMessageImpressions
|
|
const messageImpressions = Object.assign({}, state.messageImpressions); // eslint-disable-line no-shadow
|
|
messageImpressions.foo = fooMessageImpressions;
|
|
// Add barProviderImpressions
|
|
const providerImpressions = Object.assign({}, state.providerImpressions); // eslint-disable-line no-shadow
|
|
providerImpressions.bar = barProviderImpressions;
|
|
return {providers, messageImpressions, providerImpressions};
|
|
});
|
|
|
|
await Router.isBelowFrequencyCaps(message);
|
|
|
|
assert.calledTwice(Router._isBelowItemFrequencyCap);
|
|
assert.calledWithExactly(Router._isBelowItemFrequencyCap, message, fooMessageImpressions, MAX_MESSAGE_LIFETIME_CAP);
|
|
assert.calledWithExactly(Router._isBelowItemFrequencyCap, provider, barProviderImpressions);
|
|
});
|
|
});
|
|
|
|
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("handle targeting errors", () => {
|
|
it("should dispatch an event when a targeting expression throws an error", async () => {
|
|
sandbox.stub(global.FilterExpressions, "eval").returns(Promise.reject(new Error("fake error")));
|
|
await Router.setState({messages: [{id: "foo", targeting: "foo2.[[("}]});
|
|
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
|
|
dispatchStub.reset();
|
|
|
|
await Router.onMessage(msg);
|
|
|
|
assert.calledOnce(dispatchStub);
|
|
const [action] = dispatchStub.firstCall.args;
|
|
assert.equal(action.type, "AS_ROUTER_TELEMETRY_USER_EVENT");
|
|
assert.equal(action.data.message_id, "foo");
|
|
});
|
|
});
|
|
});
|