forked from mirrors/gecko-dev
Depends on D165820 Differential Revision: https://phabricator.services.mozilla.com/D165821
1900 lines
61 KiB
JavaScript
1900 lines
61 KiB
JavaScript
import { FakePrefs, GlobalOverrider } from "test/unit/utils";
|
|
import { actionTypes as at } from "common/Actions.sys.mjs";
|
|
import injector from "inject!lib/TopStoriesFeed.jsm";
|
|
|
|
describe("Top Stories Feed", () => {
|
|
let TopStoriesFeed;
|
|
let STORIES_UPDATE_TIME;
|
|
let TOPICS_UPDATE_TIME;
|
|
let SECTION_ID;
|
|
let SPOC_IMPRESSION_TRACKING_PREF;
|
|
let REC_IMPRESSION_TRACKING_PREF;
|
|
let DEFAULT_RECS_EXPIRE_TIME;
|
|
let instance;
|
|
let clock;
|
|
let globals;
|
|
let sectionsManagerStub;
|
|
let shortURLStub;
|
|
|
|
const FAKE_OPTIONS = {
|
|
stories_endpoint: "https://somedomain.org/stories?key=$apiKey",
|
|
stories_referrer: "https://somedomain.org/referrer",
|
|
topics_endpoint: "https://somedomain.org/topics?key=$apiKey",
|
|
survey_link: "https://www.surveymonkey.com/r/newtabffx",
|
|
api_key_pref: "apiKeyPref",
|
|
provider_name: "test-provider",
|
|
provider_icon: "provider-icon",
|
|
provider_description: "provider_desc",
|
|
};
|
|
|
|
beforeEach(() => {
|
|
FakePrefs.prototype.prefs.apiKeyPref = "test-api-key";
|
|
FakePrefs.prototype.prefs.pocketCta = JSON.stringify({
|
|
cta_button: "",
|
|
cta_text: "",
|
|
cta_url: "",
|
|
use_cta: false,
|
|
});
|
|
|
|
globals = new GlobalOverrider();
|
|
globals.set("PlacesUtils", { history: {} });
|
|
globals.set("pktApi", { isUserLoggedIn() {} });
|
|
clock = sinon.useFakeTimers();
|
|
shortURLStub = sinon.stub().callsFake(site => site.url);
|
|
sectionsManagerStub = {
|
|
onceInitialized: sinon.stub().callsFake(callback => callback()),
|
|
enableSection: sinon.spy(),
|
|
disableSection: sinon.spy(),
|
|
updateSection: sinon.spy(),
|
|
sections: new Map([["topstories", { options: FAKE_OPTIONS }]]),
|
|
};
|
|
|
|
({
|
|
TopStoriesFeed,
|
|
STORIES_UPDATE_TIME,
|
|
TOPICS_UPDATE_TIME,
|
|
SECTION_ID,
|
|
SPOC_IMPRESSION_TRACKING_PREF,
|
|
REC_IMPRESSION_TRACKING_PREF,
|
|
DEFAULT_RECS_EXPIRE_TIME,
|
|
} = injector({
|
|
"lib/ActivityStreamPrefs.jsm": { Prefs: FakePrefs },
|
|
"lib/ShortURL.jsm": { shortURL: shortURLStub },
|
|
"lib/SectionsManager.jsm": { SectionsManager: sectionsManagerStub },
|
|
}));
|
|
|
|
instance = new TopStoriesFeed();
|
|
instance.store = {
|
|
getState() {
|
|
return {
|
|
Prefs: {
|
|
values: {
|
|
showSponsored: true,
|
|
"feeds.section.topstories": true,
|
|
},
|
|
},
|
|
};
|
|
},
|
|
dispatch: sinon.spy(),
|
|
};
|
|
instance.storiesLastUpdated = 0;
|
|
instance.topicsLastUpdated = 0;
|
|
});
|
|
afterEach(() => {
|
|
globals.restore();
|
|
clock.restore();
|
|
});
|
|
|
|
describe("#lazyloading TopStories", () => {
|
|
beforeEach(() => {
|
|
instance.discoveryStreamEnabled = true;
|
|
});
|
|
it("should bind parseOptions to SectionsManager.onceInitialized when discovery stream is true", () => {
|
|
instance.discoveryStreamEnabled = false;
|
|
instance.store.getState = () => ({
|
|
Prefs: {
|
|
values: {
|
|
"discoverystream.config": JSON.stringify({ enabled: true }),
|
|
"feeds.section.topstories": true,
|
|
},
|
|
},
|
|
});
|
|
instance.onAction({ type: at.INIT, data: {} });
|
|
|
|
assert.calledOnce(sectionsManagerStub.onceInitialized);
|
|
});
|
|
it("should bind parseOptions to SectionsManager.onceInitialized when discovery stream is false", () => {
|
|
instance.store.getState = () => ({
|
|
Prefs: {
|
|
values: {
|
|
"discoverystream.config": JSON.stringify({ enabled: false }),
|
|
"feeds.section.topstories": true,
|
|
},
|
|
},
|
|
});
|
|
instance.onAction({ type: at.INIT, data: {} });
|
|
assert.calledOnce(sectionsManagerStub.onceInitialized);
|
|
});
|
|
it("Should initialize properties once while lazy loading if not initialized earlier", () => {
|
|
instance.discoveryStreamEnabled = false;
|
|
instance.propertiesInitialized = false;
|
|
sinon.stub(instance, "initializeProperties");
|
|
instance.lazyLoadTopStories();
|
|
assert.calledOnce(instance.initializeProperties);
|
|
});
|
|
it("should not re-initialize properties", () => {
|
|
// For discovery stream experience disabled TopStoriesFeed properties
|
|
// are initialized in constructor and should not be called again while lazy loading topstories
|
|
sinon.stub(instance, "initializeProperties");
|
|
instance.discoveryStreamEnabled = false;
|
|
instance.propertiesInitialized = true;
|
|
instance.lazyLoadTopStories();
|
|
assert.notCalled(instance.initializeProperties);
|
|
});
|
|
it("should have early exit onInit when discovery is true", async () => {
|
|
sinon.stub(instance, "doContentUpdate");
|
|
await instance.onInit();
|
|
assert.notCalled(instance.doContentUpdate);
|
|
assert.isUndefined(instance.storiesLoaded);
|
|
});
|
|
it("should complete onInit when discovery is false", async () => {
|
|
instance.discoveryStreamEnabled = false;
|
|
sinon.stub(instance, "doContentUpdate");
|
|
await instance.onInit();
|
|
assert.calledOnce(instance.doContentUpdate);
|
|
assert.isTrue(instance.storiesLoaded);
|
|
});
|
|
it("should handle limited actions when discoverystream is enabled", async () => {
|
|
sinon.spy(instance, "handleDisabled");
|
|
sinon.stub(instance, "getPocketState");
|
|
instance.store.getState = () => ({
|
|
Prefs: {
|
|
values: {
|
|
"discoverystream.config": JSON.stringify({ enabled: true }),
|
|
"discoverystream.enabled": true,
|
|
"feeds.section.topstories": true,
|
|
},
|
|
},
|
|
});
|
|
|
|
instance.onAction({ type: at.INIT, data: {} });
|
|
|
|
assert.calledOnce(instance.handleDisabled);
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.notCalled(instance.getPocketState);
|
|
});
|
|
it("should handle NEW_TAB_REHYDRATED when discoverystream is disabled", async () => {
|
|
instance.discoveryStreamEnabled = false;
|
|
sinon.spy(instance, "handleDisabled");
|
|
sinon.stub(instance, "getPocketState");
|
|
instance.store.getState = () => ({
|
|
Prefs: {
|
|
values: {
|
|
"discoverystream.config": JSON.stringify({ enabled: false }),
|
|
"feeds.section.topstories": true,
|
|
},
|
|
},
|
|
});
|
|
instance.onAction({ type: at.INIT, data: {} });
|
|
assert.notCalled(instance.handleDisabled);
|
|
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.calledOnce(instance.getPocketState);
|
|
});
|
|
it("should handle UNINIT when discoverystream is enabled", async () => {
|
|
sinon.stub(instance, "uninit");
|
|
instance.onAction({ type: at.UNINIT });
|
|
assert.calledOnce(instance.uninit);
|
|
});
|
|
it("should fire init on PREF_CHANGED", () => {
|
|
sinon.stub(instance, "onInit");
|
|
instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: { name: "discoverystream.config", value: {} },
|
|
});
|
|
assert.calledOnce(instance.onInit);
|
|
});
|
|
it("should fire init on DISCOVERY_STREAM_PREF_ENABLED", () => {
|
|
sinon.stub(instance, "onInit");
|
|
instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: { name: "discoverystream.enabled", value: true },
|
|
});
|
|
assert.calledOnce(instance.onInit);
|
|
});
|
|
it("should not fire init on PREF_CHANGED if stories are loaded", () => {
|
|
sinon.stub(instance, "onInit");
|
|
sinon.spy(instance, "lazyLoadTopStories");
|
|
instance.storiesLoaded = true;
|
|
instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: { name: "discoverystream.config", value: {} },
|
|
});
|
|
assert.calledOnce(instance.lazyLoadTopStories);
|
|
assert.notCalled(instance.onInit);
|
|
});
|
|
it("should fire init on PREF_CHANGED when discoverystream is disabled", () => {
|
|
instance.discoveryStreamEnabled = false;
|
|
sinon.stub(instance, "onInit");
|
|
instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: { name: "discoverystream.config", value: {} },
|
|
});
|
|
assert.calledOnce(instance.onInit);
|
|
});
|
|
it("should not fire init on PREF_CHANGED when discoverystream is disabled and stories are loaded", () => {
|
|
instance.discoveryStreamEnabled = false;
|
|
sinon.stub(instance, "onInit");
|
|
sinon.spy(instance, "lazyLoadTopStories");
|
|
instance.storiesLoaded = true;
|
|
instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: { name: "discoverystream.config", value: {} },
|
|
});
|
|
assert.calledOnce(instance.lazyLoadTopStories);
|
|
assert.notCalled(instance.onInit);
|
|
});
|
|
it("should not init props if ds pref is true", () => {
|
|
sinon.stub(instance, "initializeProperties");
|
|
instance.propertiesInitialized = false;
|
|
instance.store.getState = () => ({
|
|
Prefs: {
|
|
values: {
|
|
"discoverystream.config": JSON.stringify({ enabled: false }),
|
|
"discoverystream.enabled": true,
|
|
"feeds.section.topstories": true,
|
|
},
|
|
},
|
|
});
|
|
instance.lazyLoadTopStories({
|
|
dsPref: JSON.stringify({ enabled: true }),
|
|
});
|
|
assert.notCalled(instance.initializeProperties);
|
|
});
|
|
it("should fire init if user pref is true", () => {
|
|
sinon.stub(instance, "onInit");
|
|
instance.store.getState = () => ({
|
|
Prefs: {
|
|
values: {
|
|
"discoverystream.config": JSON.stringify({ enabled: false }),
|
|
"discoverystream.enabled": false,
|
|
"feeds.section.topstories": false,
|
|
},
|
|
},
|
|
});
|
|
instance.lazyLoadTopStories({ userPref: true });
|
|
assert.calledOnce(instance.onInit);
|
|
});
|
|
it("should fire uninit if topstories update to false", () => {
|
|
sinon.stub(instance, "uninit");
|
|
instance.discoveryStreamEnabled = false;
|
|
instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: {
|
|
value: false,
|
|
name: "feeds.section.topstories",
|
|
},
|
|
});
|
|
assert.calledOnce(instance.uninit);
|
|
instance.discoveryStreamEnabled = true;
|
|
instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: {
|
|
value: false,
|
|
name: "feeds.section.topstories",
|
|
},
|
|
});
|
|
assert.calledTwice(instance.uninit);
|
|
});
|
|
it("should fire lazyLoadTopstories if topstories update to true", () => {
|
|
sinon.stub(instance, "lazyLoadTopStories");
|
|
instance.discoveryStreamEnabled = false;
|
|
instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: {
|
|
value: true,
|
|
name: "feeds.section.topstories",
|
|
},
|
|
});
|
|
assert.calledOnce(instance.lazyLoadTopStories);
|
|
instance.discoveryStreamEnabled = true;
|
|
instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: {
|
|
value: true,
|
|
name: "feeds.section.topstories",
|
|
},
|
|
});
|
|
assert.calledTwice(instance.lazyLoadTopStories);
|
|
});
|
|
});
|
|
|
|
describe("#init", () => {
|
|
it("should create a TopStoriesFeed", () => {
|
|
assert.instanceOf(instance, TopStoriesFeed);
|
|
});
|
|
it("should bind parseOptions to SectionsManager.onceInitialized", () => {
|
|
instance.onAction({ type: at.INIT, data: {} });
|
|
assert.calledOnce(sectionsManagerStub.onceInitialized);
|
|
});
|
|
it("should initialize endpoints based on options", async () => {
|
|
await instance.onInit();
|
|
assert.equal(
|
|
"https://somedomain.org/stories?key=test-api-key",
|
|
instance.stories_endpoint
|
|
);
|
|
assert.equal(
|
|
"https://somedomain.org/referrer",
|
|
instance.stories_referrer
|
|
);
|
|
assert.equal(
|
|
"https://somedomain.org/topics?key=test-api-key",
|
|
instance.topics_endpoint
|
|
);
|
|
});
|
|
it("should enable its section", () => {
|
|
instance.onAction({ type: at.INIT, data: {} });
|
|
assert.calledOnce(sectionsManagerStub.enableSection);
|
|
assert.calledWith(sectionsManagerStub.enableSection, SECTION_ID);
|
|
});
|
|
it("init should fire onInit", () => {
|
|
instance.onInit = sinon.spy();
|
|
instance.onAction({ type: at.INIT, data: {} });
|
|
assert.calledOnce(instance.onInit);
|
|
});
|
|
it("should fetch stories on init", async () => {
|
|
instance.fetchStories = sinon.spy();
|
|
await instance.onInit();
|
|
assert.calledOnce(instance.fetchStories);
|
|
});
|
|
it("should fetch topics on init", async () => {
|
|
instance.fetchTopics = sinon.spy();
|
|
await instance.onInit();
|
|
assert.calledOnce(instance.fetchTopics);
|
|
});
|
|
it("should not fetch if endpoint not configured", () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
globals.set("fetch", fetchStub);
|
|
sectionsManagerStub.sections.set("topstories", { options: {} });
|
|
instance.init();
|
|
assert.notCalled(fetchStub);
|
|
});
|
|
it("should report error for invalid configuration", () => {
|
|
globals.sandbox.spy(global.console, "error");
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
api_key_pref: "invalid",
|
|
stories_endpoint: "https://invalid.com/?apiKey=$apiKey",
|
|
},
|
|
});
|
|
instance.init();
|
|
|
|
assert.calledWith(
|
|
console.error,
|
|
"Problem initializing top stories feed: An API key was specified but none configured: https://invalid.com/?apiKey=$apiKey"
|
|
);
|
|
});
|
|
it("should report error for missing api key", () => {
|
|
globals.sandbox.spy(global.console, "error");
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
stories_endpoint: "https://somedomain.org/stories?key=$apiKey",
|
|
topics_endpoint: "https://somedomain.org/topics?key=$apiKey",
|
|
},
|
|
});
|
|
instance.init();
|
|
|
|
assert.called(console.error);
|
|
});
|
|
it("should load data from cache on init", async () => {
|
|
instance.loadCachedData = sinon.spy();
|
|
await instance.onInit();
|
|
assert.calledOnce(instance.loadCachedData);
|
|
});
|
|
});
|
|
describe("#uninit", () => {
|
|
it("should disable its section", () => {
|
|
instance.onAction({ type: at.UNINIT });
|
|
assert.calledOnce(sectionsManagerStub.disableSection);
|
|
assert.calledWith(sectionsManagerStub.disableSection, SECTION_ID);
|
|
});
|
|
it("should unload stories on uninit", async () => {
|
|
sinon.stub(instance.cache, "set").returns(Promise.resolve());
|
|
await instance.clearCache();
|
|
assert.calledWith(instance.cache.set.firstCall, "stories", {});
|
|
assert.calledWith(instance.cache.set.secondCall, "topics", {});
|
|
assert.calledWith(instance.cache.set.thirdCall, "spocs", {});
|
|
});
|
|
});
|
|
describe("#cache", () => {
|
|
it("should clear all cache items when calling clearCache", () => {
|
|
sinon.stub(instance.cache, "set").returns(Promise.resolve());
|
|
instance.storiesLoaded = true;
|
|
instance.uninit();
|
|
assert.equal(instance.storiesLoaded, false);
|
|
});
|
|
it("should set spocs cache on fetch", async () => {
|
|
const response = {
|
|
recommendations: [{ id: "1" }, { id: "2" }],
|
|
settings: {},
|
|
spocs: [{ id: "spoc1" }],
|
|
};
|
|
|
|
instance.show_spocs = true;
|
|
instance.stories_endpoint = "stories-endpoint";
|
|
|
|
let fetchStub = globals.sandbox.stub();
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", { blockedLinks: { isBlocked: () => {} } });
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
sinon.spy(instance.cache, "set");
|
|
|
|
await instance.fetchStories();
|
|
|
|
assert.calledOnce(instance.cache.set);
|
|
const { args } = instance.cache.set.firstCall;
|
|
assert.equal(args[0], "stories");
|
|
assert.equal(args[1].spocs[0].id, "spoc1");
|
|
});
|
|
it("should get spocs on cache load", async () => {
|
|
instance.cache.get = () => ({
|
|
stories: {
|
|
recommendations: [{ id: "1" }, { id: "2" }],
|
|
spocs: [{ id: "spoc1" }],
|
|
},
|
|
});
|
|
instance.storiesLastUpdated = 0;
|
|
globals.set("NewTabUtils", { blockedLinks: { isBlocked: () => {} } });
|
|
|
|
await instance.loadCachedData();
|
|
assert.equal(instance.spocs[0].guid, "spoc1");
|
|
});
|
|
});
|
|
describe("#fetch", () => {
|
|
it("should fetch stories, send event and cache results", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
stories_endpoint: "stories-endpoint",
|
|
stories_referrer: "referrer",
|
|
},
|
|
});
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
|
|
const response = {
|
|
recommendations: [
|
|
{
|
|
id: "1",
|
|
title: "title",
|
|
excerpt: "description",
|
|
image_src: "image-url",
|
|
url: "rec-url",
|
|
published_timestamp: "123",
|
|
context: "trending",
|
|
icon: "icon",
|
|
},
|
|
],
|
|
};
|
|
const stories = [
|
|
{
|
|
guid: "1",
|
|
type: "now",
|
|
title: "title",
|
|
context: "trending",
|
|
icon: "icon",
|
|
description: "description",
|
|
image: "image-url",
|
|
referrer: "referrer",
|
|
url: "rec-url",
|
|
hostname: "rec-url",
|
|
score: 1,
|
|
spoc_meta: {},
|
|
},
|
|
];
|
|
|
|
instance.cache.set = sinon.spy();
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.onInit();
|
|
|
|
assert.calledOnce(fetchStub);
|
|
assert.calledOnce(shortURLStub);
|
|
assert.calledWithExactly(fetchStub, instance.stories_endpoint, {
|
|
credentials: "omit",
|
|
});
|
|
assert.calledOnce(sectionsManagerStub.updateSection);
|
|
assert.calledWith(sectionsManagerStub.updateSection, SECTION_ID, {
|
|
rows: stories,
|
|
});
|
|
assert.calledOnce(instance.cache.set);
|
|
assert.calledWith(
|
|
instance.cache.set,
|
|
"stories",
|
|
Object.assign({}, response, { _timestamp: 0 })
|
|
);
|
|
});
|
|
it("should use domain as hostname, if present", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
stories_endpoint: "stories-endpoint",
|
|
stories_referrer: "referrer",
|
|
},
|
|
});
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
|
|
const response = {
|
|
recommendations: [
|
|
{
|
|
id: "1",
|
|
title: "title",
|
|
excerpt: "description",
|
|
image_src: "image-url",
|
|
url: "rec-url",
|
|
domain: "domain",
|
|
published_timestamp: "123",
|
|
context: "trending",
|
|
icon: "icon",
|
|
},
|
|
],
|
|
};
|
|
const stories = [
|
|
{
|
|
guid: "1",
|
|
type: "now",
|
|
title: "title",
|
|
context: "trending",
|
|
icon: "icon",
|
|
description: "description",
|
|
image: "image-url",
|
|
referrer: "referrer",
|
|
url: "rec-url",
|
|
hostname: "domain",
|
|
score: 1,
|
|
spoc_meta: {},
|
|
},
|
|
];
|
|
|
|
instance.cache.set = sinon.spy();
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.onInit();
|
|
|
|
assert.calledOnce(fetchStub);
|
|
assert.notCalled(shortURLStub);
|
|
assert.calledWith(sectionsManagerStub.updateSection, SECTION_ID, {
|
|
rows: stories,
|
|
});
|
|
});
|
|
it("should call SectionsManager.updateSection", () => {
|
|
instance.dispatchUpdateEvent(123, {});
|
|
assert.calledOnce(sectionsManagerStub.updateSection);
|
|
});
|
|
it("should report error for unexpected stories response", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: { stories_endpoint: "stories-endpoint" },
|
|
});
|
|
globals.set("fetch", fetchStub);
|
|
globals.sandbox.spy(global.console, "error");
|
|
|
|
fetchStub.resolves({ ok: false, status: 400 });
|
|
await instance.onInit();
|
|
|
|
assert.calledOnce(fetchStub);
|
|
assert.calledWithExactly(fetchStub, instance.stories_endpoint, {
|
|
credentials: "omit",
|
|
});
|
|
assert.equal(instance.storiesLastUpdated, 0);
|
|
assert.called(console.error);
|
|
});
|
|
it("should exclude blocked (dismissed) URLs", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: { stories_endpoint: "stories-endpoint" },
|
|
});
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: site => site.url === "blocked" },
|
|
});
|
|
|
|
const response = {
|
|
recommendations: [{ url: "blocked" }, { url: "not_blocked" }],
|
|
};
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.onInit();
|
|
|
|
// Issue!
|
|
// Should actually be fixed when cache is fixed.
|
|
assert.calledOnce(sectionsManagerStub.updateSection);
|
|
assert.equal(
|
|
sectionsManagerStub.updateSection.firstCall.args[1].rows.length,
|
|
1
|
|
);
|
|
assert.equal(
|
|
sectionsManagerStub.updateSection.firstCall.args[1].rows[0].url,
|
|
"not_blocked"
|
|
);
|
|
});
|
|
it("should mark stories as new", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: { stories_endpoint: "stories-endpoint" },
|
|
});
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
clock.restore();
|
|
const response = {
|
|
recommendations: [
|
|
{ published_timestamp: Date.now() / 1000 },
|
|
{ published_timestamp: "0" },
|
|
{
|
|
published_timestamp: (Date.now() - 2 * 24 * 60 * 60 * 1000) / 1000,
|
|
},
|
|
],
|
|
};
|
|
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
|
|
await instance.onInit();
|
|
assert.calledOnce(sectionsManagerStub.updateSection);
|
|
assert.equal(
|
|
sectionsManagerStub.updateSection.firstCall.args[1].rows.length,
|
|
3
|
|
);
|
|
assert.equal(
|
|
sectionsManagerStub.updateSection.firstCall.args[1].rows[0].type,
|
|
"now"
|
|
);
|
|
assert.equal(
|
|
sectionsManagerStub.updateSection.firstCall.args[1].rows[1].type,
|
|
"trending"
|
|
);
|
|
assert.equal(
|
|
sectionsManagerStub.updateSection.firstCall.args[1].rows[2].type,
|
|
"trending"
|
|
);
|
|
});
|
|
it("should fetch topics, send event and cache results", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: { topics_endpoint: "topics-endpoint" },
|
|
});
|
|
globals.set("fetch", fetchStub);
|
|
|
|
const response = {
|
|
topics: [
|
|
{ name: "topic1", url: "url-topic1" },
|
|
{ name: "topic2", url: "url-topic2" },
|
|
],
|
|
};
|
|
const topics = [
|
|
{
|
|
name: "topic1",
|
|
url: "url-topic1",
|
|
},
|
|
{
|
|
name: "topic2",
|
|
url: "url-topic2",
|
|
},
|
|
];
|
|
|
|
instance.cache.set = sinon.spy();
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.onInit();
|
|
|
|
assert.calledOnce(fetchStub);
|
|
assert.calledWithExactly(fetchStub, instance.topics_endpoint, {
|
|
credentials: "omit",
|
|
});
|
|
assert.calledOnce(sectionsManagerStub.updateSection);
|
|
assert.calledWithMatch(sectionsManagerStub.updateSection, SECTION_ID, {
|
|
topics,
|
|
});
|
|
assert.calledOnce(instance.cache.set);
|
|
assert.calledWith(
|
|
instance.cache.set,
|
|
"topics",
|
|
Object.assign({}, response, { _timestamp: 0 })
|
|
);
|
|
});
|
|
it("should report error for unexpected topics response", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
globals.set("fetch", fetchStub);
|
|
globals.sandbox.spy(global.console, "error");
|
|
|
|
instance.topics_endpoint = "topics-endpoint";
|
|
fetchStub.resolves({ ok: false, status: 400 });
|
|
await instance.fetchTopics();
|
|
|
|
assert.calledOnce(fetchStub);
|
|
assert.calledWithExactly(fetchStub, instance.topics_endpoint, {
|
|
credentials: "omit",
|
|
});
|
|
assert.notCalled(instance.store.dispatch);
|
|
assert.called(console.error);
|
|
});
|
|
});
|
|
describe("#personalization", () => {
|
|
it("should sort stories", async () => {
|
|
const response = {
|
|
recommendations: [{ id: "1" }, { id: "2" }],
|
|
settings: {},
|
|
};
|
|
|
|
instance.compareScore = sinon.spy();
|
|
instance.stories_endpoint = "stories-endpoint";
|
|
|
|
let fetchStub = globals.sandbox.stub();
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
|
|
await instance.fetchStories();
|
|
assert.calledOnce(instance.compareScore);
|
|
});
|
|
it("should sort items based on relevance score", () => {
|
|
let items = [{ score: 0.1 }, { score: 0.2 }];
|
|
items = items.sort(instance.compareScore);
|
|
assert.deepEqual(items, [{ score: 0.2 }, { score: 0.1 }]);
|
|
});
|
|
it("should rotate items", () => {
|
|
let items = [
|
|
{ guid: "g1" },
|
|
{ guid: "g2" },
|
|
{ guid: "g3" },
|
|
{ guid: "g4" },
|
|
{ guid: "g5" },
|
|
{ guid: "g6" },
|
|
];
|
|
|
|
// No impressions should leave items unchanged
|
|
let rotated = instance.rotate(items);
|
|
assert.deepEqual(items, rotated);
|
|
|
|
// Recent impression should leave items unchanged
|
|
instance._prefs.get = pref =>
|
|
pref === REC_IMPRESSION_TRACKING_PREF &&
|
|
JSON.stringify({ g1: 1, g2: 1, g3: 1 });
|
|
rotated = instance.rotate(items);
|
|
assert.deepEqual(items, rotated);
|
|
|
|
// Impression older than expiration time should rotate items
|
|
clock.tick(DEFAULT_RECS_EXPIRE_TIME + 1);
|
|
rotated = instance.rotate(items);
|
|
assert.deepEqual(
|
|
[
|
|
{ guid: "g4" },
|
|
{ guid: "g5" },
|
|
{ guid: "g6" },
|
|
{ guid: "g1" },
|
|
{ guid: "g2" },
|
|
{ guid: "g3" },
|
|
],
|
|
rotated
|
|
);
|
|
|
|
instance._prefs.get = pref =>
|
|
pref === REC_IMPRESSION_TRACKING_PREF &&
|
|
JSON.stringify({
|
|
g1: 1,
|
|
g2: 1,
|
|
g3: 1,
|
|
g4: DEFAULT_RECS_EXPIRE_TIME + 1,
|
|
});
|
|
clock.tick(DEFAULT_RECS_EXPIRE_TIME);
|
|
rotated = instance.rotate(items);
|
|
assert.deepEqual(
|
|
[
|
|
{ guid: "g5" },
|
|
{ guid: "g6" },
|
|
{ guid: "g1" },
|
|
{ guid: "g2" },
|
|
{ guid: "g3" },
|
|
{ guid: "g4" },
|
|
],
|
|
rotated
|
|
);
|
|
});
|
|
it("should record top story impressions", async () => {
|
|
instance._prefs = { get: pref => undefined, set: sinon.spy() };
|
|
|
|
clock.tick(1);
|
|
let expectedPrefValue = JSON.stringify({ 1: 1, 2: 1, 3: 1 });
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: {
|
|
source: "TOP_STORIES",
|
|
tiles: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
|
},
|
|
});
|
|
assert.calledWith(
|
|
instance._prefs.set.firstCall,
|
|
REC_IMPRESSION_TRACKING_PREF,
|
|
expectedPrefValue
|
|
);
|
|
|
|
// Only need to record first impression, so impression pref shouldn't change
|
|
instance._prefs.get = pref => expectedPrefValue;
|
|
clock.tick(1);
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: {
|
|
source: "TOP_STORIES",
|
|
tiles: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
|
},
|
|
});
|
|
assert.calledOnce(instance._prefs.set);
|
|
|
|
// New first impressions should be added
|
|
clock.tick(1);
|
|
let expectedPrefValueTwo = JSON.stringify({
|
|
1: 1,
|
|
2: 1,
|
|
3: 1,
|
|
4: 3,
|
|
5: 3,
|
|
6: 3,
|
|
});
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: {
|
|
source: "TOP_STORIES",
|
|
tiles: [{ id: 4 }, { id: 5 }, { id: 6 }],
|
|
},
|
|
});
|
|
assert.calledWith(
|
|
instance._prefs.set.secondCall,
|
|
REC_IMPRESSION_TRACKING_PREF,
|
|
expectedPrefValueTwo
|
|
);
|
|
});
|
|
it("should not record top story impressions for non-view impressions", async () => {
|
|
instance._prefs = { get: pref => undefined, set: sinon.spy() };
|
|
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: { source: "TOP_STORIES", click: 0, tiles: [{ id: 1 }] },
|
|
});
|
|
assert.notCalled(instance._prefs.set);
|
|
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: { source: "TOP_STORIES", block: 0, tiles: [{ id: 1 }] },
|
|
});
|
|
assert.notCalled(instance._prefs.set);
|
|
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: { source: "TOP_STORIES", pocket: 0, tiles: [{ id: 1 }] },
|
|
});
|
|
assert.notCalled(instance._prefs.set);
|
|
});
|
|
it("should clean up top story impressions", async () => {
|
|
instance._prefs = {
|
|
get: pref => JSON.stringify({ 1: 1, 2: 1, 3: 1 }),
|
|
set: sinon.spy(),
|
|
};
|
|
|
|
let fetchStub = globals.sandbox.stub();
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
|
|
instance.stories_endpoint = "stories-endpoint";
|
|
const response = { recommendations: [{ id: 3 }, { id: 4 }, { id: 5 }] };
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.fetchStories();
|
|
|
|
// Should remove impressions for rec 1 and 2 as no longer in the feed
|
|
assert.calledWith(
|
|
instance._prefs.set.firstCall,
|
|
REC_IMPRESSION_TRACKING_PREF,
|
|
JSON.stringify({ 3: 1 })
|
|
);
|
|
});
|
|
it("should not change provider with badly formed JSON", async () => {
|
|
sinon.stub(instance, "uninit");
|
|
sinon.stub(instance, "init");
|
|
sinon.stub(instance, "clearCache").returns(Promise.resolve());
|
|
await instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: {
|
|
name: "feeds.section.topstories.options",
|
|
value: "{version: 2}",
|
|
},
|
|
});
|
|
assert.notCalled(instance.uninit);
|
|
assert.notCalled(instance.init);
|
|
assert.notCalled(instance.clearCache);
|
|
});
|
|
});
|
|
describe("#spocs", async () => {
|
|
it("should not display expired or untimestamped spocs", async () => {
|
|
clock.tick(441792000000); // 01/01/1984
|
|
|
|
instance.spocsPerNewTabs = 1;
|
|
instance.show_spocs = true;
|
|
instance.isBelowFrequencyCap = () => true;
|
|
|
|
// NOTE: `expiration_timestamp` is seconds since UNIX epoch
|
|
instance.spocs = [
|
|
// No timestamp stays visible
|
|
{
|
|
id: "spoc1",
|
|
},
|
|
// Expired spoc gets filtered out
|
|
{
|
|
id: "spoc2",
|
|
expiration_timestamp: 1,
|
|
},
|
|
// Far future expiration spoc stays visible
|
|
{
|
|
id: "spoc3",
|
|
expiration_timestamp: 32503708800, // 01/01/3000
|
|
},
|
|
];
|
|
|
|
sinon.spy(instance, "filterSpocs");
|
|
|
|
instance.filterSpocs();
|
|
|
|
assert.equal(instance.filterSpocs.firstCall.returnValue.length, 2);
|
|
assert.equal(instance.filterSpocs.firstCall.returnValue[0].id, "spoc1");
|
|
assert.equal(instance.filterSpocs.firstCall.returnValue[1].id, "spoc3");
|
|
});
|
|
it("should insert spoc with provided probability", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
|
|
const response = {
|
|
settings: { spocsPerNewTabs: 0.5 },
|
|
recommendations: [{ guid: "rec1" }, { guid: "rec2" }, { guid: "rec3" }],
|
|
// Include spocs with a expiration in the very distant future
|
|
spocs: [
|
|
{ id: "spoc1", expiration_timestamp: 9999999999999 },
|
|
{ id: "spoc2", expiration_timestamp: 9999999999999 },
|
|
],
|
|
};
|
|
|
|
instance.show_spocs = true;
|
|
instance.stories_endpoint = "stories-endpoint";
|
|
instance.storiesLoaded = true;
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.fetchStories();
|
|
|
|
instance.store.getState = () => ({
|
|
Sections: [{ id: "topstories", rows: response.recommendations }],
|
|
Prefs: { values: { showSponsored: true } },
|
|
});
|
|
|
|
globals.set("Math", {
|
|
random: () => 0.4,
|
|
min: Math.min,
|
|
});
|
|
instance.dispatchSpocDone = () => {};
|
|
instance.getPocketState = () => {};
|
|
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.calledOnce(instance.store.dispatch);
|
|
let [action] = instance.store.dispatch.firstCall.args;
|
|
|
|
assert.equal(at.SECTION_UPDATE, action.type);
|
|
assert.equal(true, action.meta.skipMain);
|
|
assert.equal(action.data.rows[0].guid, "rec1");
|
|
assert.equal(action.data.rows[1].guid, "rec2");
|
|
assert.equal(action.data.rows[2].guid, "spoc1");
|
|
// Make sure spoc is marked as pinned so it doesn't get removed when preloaded tabs refresh
|
|
assert.equal(action.data.rows[2].pinned, true);
|
|
|
|
// Second new tab shouldn't trigger a section update event (spocsPerNewTab === 0.5)
|
|
globals.set("Math", {
|
|
random: () => 0.6,
|
|
min: Math.min,
|
|
});
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.calledOnce(instance.store.dispatch);
|
|
|
|
globals.set("Math", {
|
|
random: () => 0.3,
|
|
min: Math.min,
|
|
});
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.calledTwice(instance.store.dispatch);
|
|
[action] = instance.store.dispatch.secondCall.args;
|
|
assert.equal(at.SECTION_UPDATE, action.type);
|
|
assert.equal(true, action.meta.skipMain);
|
|
assert.equal(action.data.rows[0].guid, "rec1");
|
|
assert.equal(action.data.rows[1].guid, "rec2");
|
|
assert.equal(action.data.rows[2].guid, "spoc1");
|
|
// Make sure spoc is marked as pinned so it doesn't get removed when preloaded tabs refresh
|
|
assert.equal(action.data.rows[2].pinned, true);
|
|
});
|
|
it("should delay inserting spoc if stories haven't been fetched", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
instance.dispatchSpocDone = () => {};
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
show_spocs: true,
|
|
stories_endpoint: "stories-endpoint",
|
|
},
|
|
});
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
globals.set("Math", {
|
|
random: () => 0.4,
|
|
min: Math.min,
|
|
floor: Math.floor,
|
|
});
|
|
instance.getPocketState = () => {};
|
|
instance.dispatchPocketCta = () => {};
|
|
|
|
const response = {
|
|
settings: { spocsPerNewTabs: 0.5 },
|
|
recommendations: [{ id: "rec1" }, { id: "rec2" }, { id: "rec3" }],
|
|
// Include one spoc with a expiration in the very distant future
|
|
spocs: [
|
|
{ id: "spoc1", expiration_timestamp: 9999999999999 },
|
|
{ id: "spoc2" },
|
|
],
|
|
};
|
|
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.notCalled(instance.store.dispatch);
|
|
assert.equal(instance.contentUpdateQueue.length, 1);
|
|
|
|
instance.spocsPerNewTabs = 0.5;
|
|
instance.store.getState = () => ({
|
|
Sections: [{ id: "topstories", rows: response.recommendations }],
|
|
Prefs: { values: { showSponsored: true } },
|
|
});
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
|
|
await instance.onInit();
|
|
assert.equal(instance.contentUpdateQueue.length, 0);
|
|
assert.calledOnce(instance.store.dispatch);
|
|
let [action] = instance.store.dispatch.firstCall.args;
|
|
assert.equal(action.type, at.SECTION_UPDATE);
|
|
});
|
|
it("should not insert spoc if preffed off", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
instance.dispatchSpocDone = () => {};
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
show_spocs: false,
|
|
stories_endpoint: "stories-endpoint",
|
|
},
|
|
});
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
instance.getPocketState = () => {};
|
|
instance.dispatchPocketCta = () => {};
|
|
|
|
const response = {
|
|
settings: { spocsPerNewTabs: 0.5 },
|
|
spocs: [{ id: "spoc1" }, { id: "spoc2" }],
|
|
};
|
|
sinon.spy(instance, "maybeAddSpoc");
|
|
sinon.spy(instance, "shouldShowSpocs");
|
|
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.onInit();
|
|
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.calledOnce(instance.maybeAddSpoc);
|
|
assert.calledOnce(instance.shouldShowSpocs);
|
|
assert.notCalled(instance.store.dispatch);
|
|
});
|
|
it("should call dispatchSpocDone when calling maybeAddSpoc", async () => {
|
|
instance.dispatchSpocDone = sinon.spy();
|
|
instance.storiesLoaded = true;
|
|
await instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.calledOnce(instance.dispatchSpocDone);
|
|
assert.calledWith(instance.dispatchSpocDone, {});
|
|
});
|
|
it("should fire POCKET_WAITING_FOR_SPOC action with false", () => {
|
|
instance.dispatchSpocDone({});
|
|
assert.calledOnce(instance.store.dispatch);
|
|
const [action] = instance.store.dispatch.firstCall.args;
|
|
assert.equal(action.type, "POCKET_WAITING_FOR_SPOC");
|
|
assert.equal(action.data, false);
|
|
});
|
|
it("should not insert spoc if user opted out", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
instance.dispatchSpocDone = () => {};
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
show_spocs: true,
|
|
stories_endpoint: "stories-endpoint",
|
|
},
|
|
});
|
|
instance.getPocketState = () => {};
|
|
instance.dispatchPocketCta = () => {};
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
|
|
const response = {
|
|
settings: { spocsPerNewTabs: 0.5 },
|
|
spocs: [{ id: "spoc1" }, { id: "spoc2" }],
|
|
};
|
|
|
|
instance.store.getState = () => ({
|
|
Sections: [{ id: "topstories", rows: response.recommendations }],
|
|
Prefs: { values: { showSponsored: false } },
|
|
});
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.onInit();
|
|
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.notCalled(instance.store.dispatch);
|
|
});
|
|
it("should not fail if there is no spoc", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
instance.dispatchSpocDone = () => {};
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
show_spocs: true,
|
|
stories_endpoint: "stories-endpoint",
|
|
},
|
|
});
|
|
instance.getPocketState = () => {};
|
|
instance.dispatchPocketCta = () => {};
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
globals.set("Math", {
|
|
random: () => 0.4,
|
|
min: Math.min,
|
|
});
|
|
|
|
const response = {
|
|
settings: { spocsPerNewTabs: 0.5 },
|
|
recommendations: [{ id: "rec1" }, { id: "rec2" }, { id: "rec3" }],
|
|
};
|
|
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.onInit();
|
|
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.notCalled(instance.store.dispatch);
|
|
});
|
|
it("should record spoc/campaign impressions for frequency capping", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
globals.set("Math", {
|
|
random: () => 0.4,
|
|
min: Math.min,
|
|
floor: Math.floor,
|
|
});
|
|
|
|
const response = {
|
|
settings: { spocsPerNewTabs: 0.5 },
|
|
spocs: [
|
|
{ id: 1, campaign_id: 5 },
|
|
{ id: 4, campaign_id: 6 },
|
|
],
|
|
};
|
|
|
|
instance._prefs = { get: pref => undefined, set: sinon.spy() };
|
|
instance.show_spocs = true;
|
|
instance.stories_endpoint = "stories-endpoint";
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.fetchStories();
|
|
|
|
let expectedPrefValue = JSON.stringify({ 5: [0] });
|
|
let expectedPrefValueCallTwo = JSON.stringify({ 2: 0, 3: 0 });
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: {
|
|
source: "TOP_STORIES",
|
|
tiles: [{ id: 3 }, { id: 2 }, { id: 1 }],
|
|
},
|
|
});
|
|
assert.calledWith(
|
|
instance._prefs.set.firstCall,
|
|
SPOC_IMPRESSION_TRACKING_PREF,
|
|
expectedPrefValue
|
|
);
|
|
assert.calledWith(
|
|
instance._prefs.set.secondCall,
|
|
REC_IMPRESSION_TRACKING_PREF,
|
|
expectedPrefValueCallTwo
|
|
);
|
|
|
|
clock.tick(1);
|
|
instance._prefs.get = pref => expectedPrefValue;
|
|
let expectedPrefValueCallThree = JSON.stringify({ 5: [0, 1] });
|
|
let expectedPrefValueCallFour = JSON.stringify({ 2: 1, 3: 1, 5: [0] });
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: {
|
|
source: "TOP_STORIES",
|
|
tiles: [{ id: 3 }, { id: 2 }, { id: 1 }],
|
|
},
|
|
});
|
|
assert.calledWith(
|
|
instance._prefs.set.thirdCall,
|
|
SPOC_IMPRESSION_TRACKING_PREF,
|
|
expectedPrefValueCallThree
|
|
);
|
|
assert.calledWith(
|
|
instance._prefs.set.getCall(3),
|
|
REC_IMPRESSION_TRACKING_PREF,
|
|
expectedPrefValueCallFour
|
|
);
|
|
|
|
clock.tick(1);
|
|
instance._prefs.get = pref => expectedPrefValueCallThree;
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: {
|
|
source: "TOP_STORIES",
|
|
tiles: [{ id: 3 }, { id: 2 }, { id: 4 }],
|
|
},
|
|
});
|
|
assert.calledWith(
|
|
instance._prefs.set.getCall(4),
|
|
SPOC_IMPRESSION_TRACKING_PREF,
|
|
JSON.stringify({ 5: [0, 1], 6: [2] })
|
|
);
|
|
assert.calledWith(
|
|
instance._prefs.set.getCall(5),
|
|
REC_IMPRESSION_TRACKING_PREF,
|
|
JSON.stringify({ 2: 2, 3: 2, 5: [0, 1] })
|
|
);
|
|
});
|
|
it("should not record spoc/campaign impressions for non-view impressions", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
show_spocs: true,
|
|
stories_endpoint: "stories-endpoint",
|
|
},
|
|
});
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
|
|
const response = {
|
|
settings: { spocsPerNewTabs: 0.5 },
|
|
spocs: [
|
|
{ id: 1, campaign_id: 5 },
|
|
{ id: 4, campaign_id: 6 },
|
|
],
|
|
};
|
|
|
|
instance._prefs = { get: pref => undefined, set: sinon.spy() };
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.onInit();
|
|
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: { source: "TOP_STORIES", click: 0, tiles: [{ id: 1 }] },
|
|
});
|
|
assert.notCalled(instance._prefs.set);
|
|
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: { source: "TOP_STORIES", block: 0, tiles: [{ id: 1 }] },
|
|
});
|
|
assert.notCalled(instance._prefs.set);
|
|
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: { source: "TOP_STORIES", pocket: 0, tiles: [{ id: 1 }] },
|
|
});
|
|
assert.notCalled(instance._prefs.set);
|
|
});
|
|
it("should clean up spoc/campaign impressions", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
|
|
instance._prefs = { get: pref => undefined, set: sinon.spy() };
|
|
instance.show_spocs = true;
|
|
instance.stories_endpoint = "stories-endpoint";
|
|
|
|
const response = {
|
|
settings: { spocsPerNewTabs: 0.5 },
|
|
spocs: [
|
|
{ id: 1, campaign_id: 5 },
|
|
{ id: 4, campaign_id: 6 },
|
|
],
|
|
};
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.fetchStories();
|
|
|
|
// simulate impressions for campaign 5 and 6
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: {
|
|
source: "TOP_STORIES",
|
|
tiles: [{ id: 3 }, { id: 2 }, { id: 1 }],
|
|
},
|
|
});
|
|
instance._prefs.get = pref =>
|
|
pref === SPOC_IMPRESSION_TRACKING_PREF && JSON.stringify({ 5: [0] });
|
|
instance.onAction({
|
|
type: at.TELEMETRY_IMPRESSION_STATS,
|
|
data: {
|
|
source: "TOP_STORIES",
|
|
tiles: [{ id: 3 }, { id: 2 }, { id: 4 }],
|
|
},
|
|
});
|
|
|
|
let expectedPrefValue = JSON.stringify({ 5: [0], 6: [0] });
|
|
assert.calledWith(
|
|
instance._prefs.set.thirdCall,
|
|
SPOC_IMPRESSION_TRACKING_PREF,
|
|
expectedPrefValue
|
|
);
|
|
instance._prefs.get = pref =>
|
|
pref === SPOC_IMPRESSION_TRACKING_PREF && expectedPrefValue;
|
|
|
|
// remove campaign 5 from response
|
|
const updatedResponse = {
|
|
settings: { spocsPerNewTabs: 1 },
|
|
spocs: [{ id: 4, campaign_id: 6 }],
|
|
};
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(updatedResponse),
|
|
});
|
|
await instance.fetchStories();
|
|
|
|
// should remove campaign 5 from pref as no longer active
|
|
assert.calledWith(
|
|
instance._prefs.set.getCall(4),
|
|
SPOC_IMPRESSION_TRACKING_PREF,
|
|
JSON.stringify({ 6: [0] })
|
|
);
|
|
});
|
|
it("should maintain frequency caps when inserting spocs", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
instance.dispatchSpocDone = () => {};
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
show_spocs: true,
|
|
stories_endpoint: "stories-endpoint",
|
|
},
|
|
});
|
|
instance.getPocketState = () => {};
|
|
instance.dispatchPocketCta = () => {};
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
|
|
const response = {
|
|
settings: { spocsPerNewTabs: 1 },
|
|
recommendations: [{ guid: "rec1" }, { guid: "rec2" }, { guid: "rec3" }],
|
|
spocs: [
|
|
// Set spoc `expiration_timestamp`s in the very distant future to ensure they show up
|
|
{
|
|
id: "spoc1",
|
|
campaign_id: 1,
|
|
caps: { lifetime: 3, campaign: { count: 2, period: 3600 } },
|
|
expiration_timestamp: 999999999999,
|
|
},
|
|
{
|
|
id: "spoc2",
|
|
campaign_id: 2,
|
|
caps: { lifetime: 1 },
|
|
expiration_timestamp: 999999999999,
|
|
},
|
|
],
|
|
};
|
|
|
|
instance.store.getState = () => ({
|
|
Sections: [{ id: "topstories", rows: response.recommendations }],
|
|
Prefs: { values: { showSponsored: true } },
|
|
});
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.onInit();
|
|
instance.spocsPerNewTabs = 1;
|
|
|
|
clock.tick();
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
let [action] = instance.store.dispatch.firstCall.args;
|
|
assert.equal(action.data.rows[0].guid, "rec1");
|
|
assert.equal(action.data.rows[1].guid, "rec2");
|
|
assert.equal(action.data.rows[2].guid, "spoc1");
|
|
instance._prefs.get = pref => JSON.stringify({ 1: [1] });
|
|
|
|
clock.tick();
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
[action] = instance.store.dispatch.secondCall.args;
|
|
assert.equal(action.data.rows[0].guid, "rec1");
|
|
assert.equal(action.data.rows[1].guid, "rec2");
|
|
assert.equal(action.data.rows[2].guid, "spoc1");
|
|
instance._prefs.get = pref => JSON.stringify({ 1: [1, 2] });
|
|
|
|
// campaign 1 period frequency cap now reached (spoc 2 should be shown)
|
|
clock.tick();
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
[action] = instance.store.dispatch.thirdCall.args;
|
|
assert.equal(action.data.rows[0].guid, "rec1");
|
|
assert.equal(action.data.rows[1].guid, "rec2");
|
|
assert.equal(action.data.rows[2].guid, "spoc2");
|
|
instance._prefs.get = pref => JSON.stringify({ 1: [1, 2], 2: [3] });
|
|
|
|
// new campaign 1 period starting (spoc 1 sohuld be shown again)
|
|
clock.tick(2 * 60 * 60 * 1000);
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
[action] = instance.store.dispatch.lastCall.args;
|
|
assert.equal(action.data.rows[0].guid, "rec1");
|
|
assert.equal(action.data.rows[1].guid, "rec2");
|
|
assert.equal(action.data.rows[2].guid, "spoc1");
|
|
instance._prefs.get = pref =>
|
|
JSON.stringify({ 1: [1, 2, 7200003], 2: [3] });
|
|
|
|
// campaign 1 lifetime cap now reached (no spoc should be sent)
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.callCount(instance.store.dispatch, 4);
|
|
});
|
|
it("should maintain client-side MAX_LIFETIME_CAP", async () => {
|
|
let fetchStub = globals.sandbox.stub();
|
|
instance.dispatchSpocDone = () => {};
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: {
|
|
show_spocs: true,
|
|
stories_endpoint: "stories-endpoint",
|
|
},
|
|
});
|
|
globals.set("fetch", fetchStub);
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
instance.getPocketState = () => {};
|
|
instance.dispatchPocketCta = () => {};
|
|
|
|
const response = {
|
|
settings: { spocsPerNewTabs: 1 },
|
|
recommendations: [{ guid: "rec1" }, { guid: "rec2" }, { guid: "rec3" }],
|
|
spocs: [{ id: "spoc1", campaign_id: 1, caps: { lifetime: 501 } }],
|
|
};
|
|
|
|
instance.store.getState = () => ({
|
|
Sections: [{ id: "topstories", rows: response.recommendations }],
|
|
Prefs: { values: { showSponsored: true } },
|
|
});
|
|
fetchStub.resolves({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(response),
|
|
});
|
|
await instance.onInit();
|
|
|
|
instance._prefs.get = pref =>
|
|
JSON.stringify({ 1: [...Array(500).keys()] });
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.notCalled(instance.store.dispatch);
|
|
});
|
|
});
|
|
describe("#update", () => {
|
|
it("should fetch stories after update interval", async () => {
|
|
await instance.onInit();
|
|
sinon.spy(instance, "fetchStories");
|
|
await instance.onAction({ type: at.SYSTEM_TICK });
|
|
assert.notCalled(instance.fetchStories);
|
|
|
|
clock.tick(STORIES_UPDATE_TIME);
|
|
await instance.onAction({ type: at.SYSTEM_TICK });
|
|
assert.calledOnce(instance.fetchStories);
|
|
});
|
|
it("should fetch topics after update interval", async () => {
|
|
await instance.onInit();
|
|
sinon.spy(instance, "fetchTopics");
|
|
await instance.onAction({ type: at.SYSTEM_TICK });
|
|
assert.notCalled(instance.fetchTopics);
|
|
|
|
clock.tick(TOPICS_UPDATE_TIME);
|
|
await instance.onAction({ type: at.SYSTEM_TICK });
|
|
assert.calledOnce(instance.fetchTopics);
|
|
});
|
|
it("should return updated stories and topics on system tick", async () => {
|
|
await instance.onInit();
|
|
sinon.spy(instance, "dispatchUpdateEvent");
|
|
const stories = [{ guid: "rec1" }, { guid: "rec2" }, { guid: "rec3" }];
|
|
const topics = [
|
|
{ name: "topic1", url: "url-topic1" },
|
|
{ name: "topic2", url: "url-topic2" },
|
|
];
|
|
clock.tick(TOPICS_UPDATE_TIME);
|
|
globals.sandbox.stub(instance, "fetchStories").resolves(stories);
|
|
globals.sandbox.stub(instance, "fetchTopics").resolves(topics);
|
|
|
|
await instance.onAction({ type: at.SYSTEM_TICK });
|
|
|
|
assert.calledOnce(instance.dispatchUpdateEvent);
|
|
assert.calledWith(instance.dispatchUpdateEvent, false, {
|
|
rows: [{ guid: "rec1" }, { guid: "rec2" }, { guid: "rec3" }],
|
|
topics: [
|
|
{ name: "topic1", url: "url-topic1" },
|
|
{ name: "topic2", url: "url-topic2" },
|
|
],
|
|
read_more_endpoint: undefined,
|
|
});
|
|
});
|
|
it("should not call init and uninit if data doesn't match on options change ", () => {
|
|
sinon.spy(instance, "init");
|
|
sinon.spy(instance, "uninit");
|
|
instance.onAction({ type: at.SECTION_OPTIONS_CHANGED, data: "foo" });
|
|
assert.notCalled(sectionsManagerStub.disableSection);
|
|
assert.notCalled(sectionsManagerStub.enableSection);
|
|
assert.notCalled(instance.init);
|
|
assert.notCalled(instance.uninit);
|
|
});
|
|
it("should call init and uninit on options change", async () => {
|
|
sinon.stub(instance, "clearCache").returns(Promise.resolve());
|
|
sinon.spy(instance, "init");
|
|
sinon.spy(instance, "uninit");
|
|
await instance.onAction({
|
|
type: at.SECTION_OPTIONS_CHANGED,
|
|
data: "topstories",
|
|
});
|
|
assert.calledOnce(sectionsManagerStub.disableSection);
|
|
assert.calledOnce(sectionsManagerStub.enableSection);
|
|
assert.calledOnce(instance.clearCache);
|
|
assert.calledOnce(instance.init);
|
|
assert.calledOnce(instance.uninit);
|
|
});
|
|
it("should set LastUpdated to 0 on init", async () => {
|
|
instance.storiesLastUpdated = 1;
|
|
instance.topicsLastUpdated = 1;
|
|
|
|
await instance.onInit();
|
|
assert.equal(instance.storiesLastUpdated, 0);
|
|
assert.equal(instance.topicsLastUpdated, 0);
|
|
});
|
|
it("should filter spocs when link is blocked", async () => {
|
|
instance.spocs = [{ url: "not_blocked" }, { url: "blocked" }];
|
|
await instance.onAction({
|
|
type: at.PLACES_LINK_BLOCKED,
|
|
data: { url: "blocked" },
|
|
});
|
|
|
|
assert.deepEqual(instance.spocs, [{ url: "not_blocked" }]);
|
|
});
|
|
});
|
|
describe("#loadCachedData", () => {
|
|
it("should update section with cached stories and topics if available", async () => {
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: { stories_referrer: "referrer" },
|
|
});
|
|
const stories = {
|
|
_timestamp: 123,
|
|
recommendations: [
|
|
{
|
|
id: "1",
|
|
title: "title",
|
|
excerpt: "description",
|
|
image_src: "image-url",
|
|
url: "rec-url",
|
|
published_timestamp: "123",
|
|
context: "trending",
|
|
icon: "icon",
|
|
item_score: 0.98,
|
|
},
|
|
],
|
|
};
|
|
const transformedStories = [
|
|
{
|
|
guid: "1",
|
|
type: "now",
|
|
title: "title",
|
|
context: "trending",
|
|
icon: "icon",
|
|
description: "description",
|
|
image: "image-url",
|
|
referrer: "referrer",
|
|
url: "rec-url",
|
|
hostname: "rec-url",
|
|
score: 0.98,
|
|
spoc_meta: {},
|
|
},
|
|
];
|
|
const topics = {
|
|
_timestamp: 123,
|
|
topics: [
|
|
{ name: "topic1", url: "url-topic1" },
|
|
{ name: "topic2", url: "url-topic2" },
|
|
],
|
|
};
|
|
instance.cache.get = () => ({ stories, topics });
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
|
|
await instance.onInit();
|
|
assert.calledOnce(sectionsManagerStub.updateSection);
|
|
assert.calledWith(sectionsManagerStub.updateSection, SECTION_ID, {
|
|
rows: transformedStories,
|
|
topics: topics.topics,
|
|
read_more_endpoint: undefined,
|
|
});
|
|
});
|
|
it("should NOT update section if there is no cached data", async () => {
|
|
instance.cache.get = () => ({});
|
|
globals.set("NewTabUtils", {
|
|
blockedLinks: { isBlocked: globals.sandbox.spy() },
|
|
});
|
|
await instance.loadCachedData();
|
|
assert.notCalled(sectionsManagerStub.updateSection);
|
|
});
|
|
it("should use store rows if no stories sent to doContentUpdate", async () => {
|
|
instance.store = {
|
|
getState() {
|
|
return {
|
|
Sections: [{ id: "topstories", rows: [1, 2, 3] }],
|
|
};
|
|
},
|
|
};
|
|
sinon.spy(instance, "dispatchUpdateEvent");
|
|
|
|
instance.doContentUpdate({}, false);
|
|
|
|
assert.calledOnce(instance.dispatchUpdateEvent);
|
|
assert.calledWith(instance.dispatchUpdateEvent, false, {
|
|
rows: [1, 2, 3],
|
|
});
|
|
});
|
|
it("should broadcast in doContentUpdate when updating from cache", async () => {
|
|
sectionsManagerStub.sections.set("topstories", {
|
|
options: { stories_referrer: "referrer" },
|
|
});
|
|
globals.set("NewTabUtils", { blockedLinks: { isBlocked: () => {} } });
|
|
const stories = { recommendations: [{}] };
|
|
const topics = { topics: [{}] };
|
|
sinon.spy(instance, "doContentUpdate");
|
|
instance.cache.get = () => ({ stories, topics });
|
|
await instance.onInit();
|
|
assert.calledOnce(instance.doContentUpdate);
|
|
assert.calledWith(
|
|
instance.doContentUpdate,
|
|
{
|
|
stories: [
|
|
{
|
|
context: undefined,
|
|
description: undefined,
|
|
guid: undefined,
|
|
hostname: undefined,
|
|
icon: undefined,
|
|
image: undefined,
|
|
referrer: "referrer",
|
|
score: 1,
|
|
spoc_meta: {},
|
|
title: undefined,
|
|
type: "trending",
|
|
url: undefined,
|
|
},
|
|
],
|
|
topics: [{}],
|
|
},
|
|
true
|
|
);
|
|
});
|
|
});
|
|
describe("#pocket", () => {
|
|
it("should call getPocketState when hitting NEW_TAB_REHYDRATED", () => {
|
|
instance.getPocketState = sinon.spy();
|
|
instance.onAction({
|
|
type: at.NEW_TAB_REHYDRATED,
|
|
meta: { fromTarget: {} },
|
|
});
|
|
assert.calledOnce(instance.getPocketState);
|
|
assert.calledWith(instance.getPocketState, {});
|
|
});
|
|
it("should call dispatch in getPocketState", () => {
|
|
const isUserLoggedIn = sinon.spy();
|
|
globals.set("pktApi", { isUserLoggedIn });
|
|
instance.getPocketState({});
|
|
assert.calledOnce(instance.store.dispatch);
|
|
const [action] = instance.store.dispatch.firstCall.args;
|
|
assert.equal(action.type, "POCKET_LOGGED_IN");
|
|
assert.calledOnce(isUserLoggedIn);
|
|
});
|
|
it("should call dispatchPocketCta when hitting onInit", async () => {
|
|
instance.dispatchPocketCta = sinon.spy();
|
|
await instance.onInit();
|
|
assert.calledOnce(instance.dispatchPocketCta);
|
|
assert.calledWith(
|
|
instance.dispatchPocketCta,
|
|
JSON.stringify({
|
|
cta_button: "",
|
|
cta_text: "",
|
|
cta_url: "",
|
|
use_cta: false,
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
it("should call dispatch in dispatchPocketCta", () => {
|
|
instance.dispatchPocketCta(JSON.stringify({ use_cta: true }), false);
|
|
assert.calledOnce(instance.store.dispatch);
|
|
const [action] = instance.store.dispatch.firstCall.args;
|
|
assert.equal(action.type, "POCKET_CTA");
|
|
assert.equal(action.data.use_cta, true);
|
|
});
|
|
it("should call dispatchPocketCta with a pocketCta pref change", () => {
|
|
instance.dispatchPocketCta = sinon.spy();
|
|
instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: {
|
|
name: "pocketCta",
|
|
value: JSON.stringify({
|
|
cta_button: "",
|
|
cta_text: "",
|
|
cta_url: "",
|
|
use_cta: false,
|
|
}),
|
|
},
|
|
});
|
|
assert.calledOnce(instance.dispatchPocketCta);
|
|
assert.calledWith(
|
|
instance.dispatchPocketCta,
|
|
JSON.stringify({
|
|
cta_button: "",
|
|
cta_text: "",
|
|
cta_url: "",
|
|
use_cta: false,
|
|
}),
|
|
true
|
|
);
|
|
});
|
|
});
|
|
it("should call uninit and init on disabling of showSponsored pref", async () => {
|
|
sinon.stub(instance, "clearCache").returns(Promise.resolve());
|
|
sinon.stub(instance, "uninit");
|
|
sinon.stub(instance, "init");
|
|
await instance.onAction({
|
|
type: at.PREF_CHANGED,
|
|
data: { name: "showSponsored", value: false },
|
|
});
|
|
assert.calledOnce(instance.clearCache);
|
|
assert.calledOnce(instance.uninit);
|
|
assert.calledOnce(instance.init);
|
|
});
|
|
});
|