Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import { combineReducers, createStore } from "redux";
import { actionTypes as at } from "common/Actions.mjs";
import { reducers } from "common/Reducers.sys.mjs";
import { selectLayoutRender } from "content-src/lib/selectLayoutRender";
const FAKE_LAYOUT = [
{
width: 3,
components: [
{ type: "foo", feed: { url: "foo.com" }, properties: { items: 2 } },
],
},
];
const FAKE_FEEDS = {
"foo.com": { data: { recommendations: [{ id: "foo" }, { id: "bar" }] } },
};
describe("selectLayoutRender", () => {
let store;
beforeEach(() => {
store = createStore(combineReducers(reducers));
});
const SPONSORED_STORIES_PREFS = {
showSponsored: true,
"system.showSponsored": true,
};
it("should return an empty array given initial state", () => {
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: {},
rollCache: [],
});
expect(layoutRender).toEqual([]);
});
it("should add .data property from feeds to each component in .layout", () => {
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: FAKE_LAYOUT },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: FAKE_FEEDS["foo.com"], url: "foo.com" },
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
expect(layoutRender).toHaveLength(1);
expect(layoutRender[0]).toHaveProperty("width", 3);
expect(layoutRender[0].components[0]).toEqual({
type: "foo",
feed: { url: "foo.com" },
properties: { items: 2 },
data: {
recommendations: [
{ id: "foo", pos: 0 },
{ id: "bar", pos: 1 },
],
sections: [],
},
});
});
it("should return layout with placeholder data if feed doesn't have data", () => {
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: FAKE_LAYOUT },
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
expect(layoutRender).toHaveLength(1);
expect(layoutRender[0]).toHaveProperty("width", 3);
expect(layoutRender[0].components[0].data.recommendations).toEqual([
{ placeholder: true },
{ placeholder: true },
]);
});
it("should return layout with empty spocs data if feed isn't defined but spocs is", () => {
const fakeLayout = [
{
width: 3,
components: [{ type: "foo", spocs: { positions: [{ index: 2 }] } }],
},
];
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
expect(layoutRender).toHaveLength(1);
expect(layoutRender[0]).toHaveProperty("width", 3);
expect(layoutRender[0].components[0].data.spocs).toEqual([]);
});
it("should return layout with spocs data if feed isn't defined but spocs is", () => {
const fakeLayout = [
{
width: 3,
components: [{ type: "foo", spocs: { positions: [{ index: 0 }] } }],
},
];
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
store.dispatch({
type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
data: {
lastUpdated: 0,
spocs: {
newtab_spocs: {
items: [{ id: 1 }, { id: 2 }, { id: 3 }],
},
},
},
});
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
expect(layoutRender).toHaveLength(1);
expect(layoutRender[0]).toHaveProperty("width", 3);
expect(layoutRender[0].components[0].data.spocs).toEqual([
{ id: 1, pos: 0 },
{ id: 2, pos: 1 },
{ id: 3, pos: 2 },
]);
});
it("should return layout with no spocs data if feed and spocs are unavailable", () => {
const fakeLayout = [
{
width: 3,
components: [{ type: "foo", spocs: { positions: [{ index: 0 }] } }],
},
];
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
store.dispatch({
type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
data: {
lastUpdated: 0,
spocs: {
spocs: {
items: [],
},
},
},
});
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
expect(layoutRender).toHaveLength(1);
expect(layoutRender[0]).toHaveProperty("width", 3);
expect(layoutRender[0].components[0].data.spocs.length).toBe(0);
});
it("should return feed data offset by layout set prop", () => {
const fakeLayout = [
{
width: 3,
components: [
{ type: "foo", properties: { offset: 1 }, feed: { url: "foo.com" } },
],
},
];
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: FAKE_FEEDS["foo.com"], url: "foo.com" },
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
expect(layoutRender[0].components[0].data).toEqual({
recommendations: [{ id: "bar" }],
sections: [],
});
});
it("should return spoc result when there are more positions than spocs", () => {
const fakeSpocConfig = {
positions: [{ index: 0 }, { index: 1 }, { index: 2 }],
};
const fakeLayout = [
{
width: 3,
components: [
{ type: "foo", feed: { url: "foo.com" }, spocs: fakeSpocConfig },
],
},
];
const fakeSpocsData = {
lastUpdated: 0,
spocs: {
newtab_spocs: { items: [{ id: "fooSpoc" }, { id: "barSpoc" }] },
},
};
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: FAKE_FEEDS["foo.com"], url: "foo.com" },
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
store.dispatch({
type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
data: fakeSpocsData,
});
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: SPONSORED_STORIES_PREFS,
});
expect(layoutRender).toHaveLength(1);
expect(layoutRender[0].components[0].data.recommendations[0]).toEqual({
id: "fooSpoc",
is_ad_eligible_position: true,
});
expect(layoutRender[0].components[0].data.recommendations[1]).toEqual({
id: "barSpoc",
is_ad_eligible_position: true,
});
expect(layoutRender[0].components[0].data.recommendations[2]).toEqual({
id: "foo",
is_ad_eligible_position: true,
});
expect(layoutRender[0].components[0].data.recommendations[3]).toEqual({
id: "bar",
});
});
it("should return a layout with feeds of items length with positions", () => {
const fakeLayout = [
{
width: 3,
components: [
{ type: "foo", properties: { items: 3 }, feed: { url: "foo.com" } },
],
},
];
const fakeRecommendations = [
{ name: "item1" },
{ name: "item2" },
{ name: "item3" },
{ name: "item4" },
];
const fakeFeeds = {
"foo.com": { data: { recommendations: fakeRecommendations } },
};
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: fakeFeeds["foo.com"], url: "foo.com" },
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
const { recommendations } = layoutRender[0].components[0].data;
expect(recommendations.length).toBe(4);
expect(recommendations[0].pos).toBe(0);
expect(recommendations[1].pos).toBe(1);
expect(recommendations[2].pos).toBe(2);
expect(recommendations[3].pos).toBe(undefined);
});
it("should render everything if everything is ready", () => {
const fakeLayout = [
{
width: 3,
components: [
{ type: "foo1" },
{ type: "foo2", properties: { items: 3 }, feed: { url: "foo2.com" } },
{ type: "foo3", properties: { items: 3 }, feed: { url: "foo3.com" } },
{ type: "foo4", properties: { items: 3 }, feed: { url: "foo4.com" } },
{ type: "foo5" },
],
},
];
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: { data: { recommendations: [] } }, url: "foo2.com" },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: { data: { recommendations: [] } }, url: "foo3.com" },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: { data: { recommendations: [] } }, url: "foo4.com" },
});
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
expect(layoutRender[0].components[0].type).toBe("foo1");
expect(layoutRender[0].components[1].type).toBe("foo2");
expect(layoutRender[0].components[2].type).toBe("foo3");
expect(layoutRender[0].components[3].type).toBe("foo4");
expect(layoutRender[0].components[4].type).toBe("foo5");
});
it("should stop rendering feeds if we hit a not ready spoc", () => {
const fakeLayout = [
{
width: 3,
components: [
{ type: "foo1" },
{ type: "foo2", properties: { items: 3 }, feed: { url: "foo2.com" } },
{
type: "foo3",
properties: { items: 3 },
feed: { url: "foo3.com" },
spocs: { positions: [{ index: 0 }] },
},
{ type: "foo4", properties: { items: 3 }, feed: { url: "foo4.com" } },
{ type: "foo5" },
],
},
];
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: { data: { recommendations: [] } }, url: "foo2.com" },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: { data: { recommendations: [] } }, url: "foo3.com" },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: { data: { recommendations: [] } }, url: "foo4.com" },
});
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
});
expect(layoutRender[0].components[0].type).toBe("foo1");
expect(layoutRender[0].components[1].type).toBe("foo2");
expect(layoutRender[0].components[2].data.recommendations).toEqual([
{ placeholder: true },
{ placeholder: true },
{ placeholder: true },
]);
});
it("should not flag ad-eligible positions when sponsored stories are off", () => {
const fakeSpocConfig = { positions: [{ index: 0 }] };
const fakeLayout = [
{
width: 3,
components: [
{ type: "foo", feed: { url: "foo.com" }, spocs: fakeSpocConfig },
],
},
];
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: {
feed: { data: { recommendations: [{ name: "rec" }] } },
url: "foo.com",
},
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
store.dispatch({
type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
data: { lastUpdated: 0, spocs: { newtab_spocs: { items: [] } } },
});
for (const prefs of [
{ showSponsored: false, "system.showSponsored": true },
{ showSponsored: true, "system.showSponsored": false },
]) {
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs,
});
expect(
layoutRender[0].components[0].data.recommendations[0]
.is_ad_eligible_position
).toBeUndefined();
}
});
it("should not render a spoc if there are no available spocs", () => {
const fakeLayout = [
{
width: 3,
components: [
{ type: "foo1" },
{ type: "foo2", properties: { items: 3 }, feed: { url: "foo2.com" } },
{
type: "foo3",
properties: { items: 3 },
feed: { url: "foo3.com" },
spocs: { positions: [{ index: 0 }] },
},
{ type: "foo4", properties: { items: 3 }, feed: { url: "foo4.com" } },
{ type: "foo5" },
],
},
];
const fakeSpocsData = { lastUpdated: 0, spocs: { spocs: [] } };
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: { data: { recommendations: [] } }, url: "foo2.com" },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: {
feed: { data: { recommendations: [{ name: "rec" }] } },
url: "foo3.com",
},
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: { feed: { data: { recommendations: [] } }, url: "foo4.com" },
});
store.dispatch({
type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
data: fakeSpocsData,
});
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: SPONSORED_STORIES_PREFS,
});
expect(layoutRender[0].components[2].data.recommendations[0]).toEqual({
name: "rec",
pos: 0,
is_ad_eligible_position: true,
});
});
it("should not render a row if no components exist after filter in that row", () => {
const fakeLayout = [
{
width: 3,
components: [{ type: "TopSites" }],
},
{
width: 3,
components: [{ type: "Message" }],
},
];
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: { "feeds.topsites": true },
});
expect(layoutRender[0].components[0].type).toBe("TopSites");
expect(layoutRender[1]).toBe(undefined);
});
it("should not render a component if filtered", () => {
const fakeLayout = [
{
width: 3,
components: [{ type: "Message" }, { type: "TopSites" }],
},
];
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: { "feeds.topsites": true },
});
expect(layoutRender[0].components[0].type).toBe("TopSites");
expect(layoutRender[0].components[1]).toBe(undefined);
});
it("should skip rendering a spoc in position if that spoc is blocked for that session", () => {
const fakeLayout = [
{
width: 3,
components: [
{
type: "foo1",
properties: { items: 3 },
feed: { url: "foo1.com" },
spocs: { positions: [{ index: 0 }] },
},
],
},
];
const fakeSpocsData = {
lastUpdated: 0,
spocs: {
newtab_spocs: { items: [{ name: "spoc", url: "https://foo.com" }] },
},
};
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: {
feed: { data: { recommendations: [{ name: "rec" }] } },
url: "foo1.com",
},
});
store.dispatch({
type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
data: fakeSpocsData,
});
const { layoutRender: layout1 } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: SPONSORED_STORIES_PREFS,
});
store.dispatch({
type: at.DISCOVERY_STREAM_SPOC_BLOCKED,
data: { url: "https://foo.com" },
});
const { layoutRender: layout2 } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: SPONSORED_STORIES_PREFS,
});
expect(layout1[0].components[0].data.recommendations[0]).toEqual({
name: "spoc",
url: "https://foo.com",
pos: 0,
is_ad_eligible_position: true,
});
expect(layout2[0].components[0].data.recommendations[0]).toEqual({
name: "rec",
pos: 0,
is_ad_eligible_position: true,
});
});
it("should include Widgets when widgets.system.enabled is true", () => {
const { layoutRender } = selectLayoutRender({
prefs: {
"widgets.system.enabled": true,
"feeds.section.topstories": false,
"feeds.system.topstories": false,
},
state: {
layout: [
{
width: 12,
components: [{ type: "Widgets" }],
},
],
},
});
expect(layoutRender).toHaveLength(1);
expect(layoutRender[0].components).toHaveLength(1);
expect(layoutRender[0].components[0]).toHaveProperty("type", "Widgets");
});
it("should include Widgets when (Nimbus) widgetsConfig.enabled is true", () => {
const { layoutRender } = selectLayoutRender({
prefs: {
widgetsConfig: { enabled: true },
"feeds.section.topstories": false,
"feeds.system.topstories": false,
},
state: {
layout: [
{
width: 12,
components: [{ type: "Widgets" }],
},
],
},
});
expect(layoutRender).toHaveLength(1);
expect(layoutRender[0].components).toHaveLength(1);
expect(layoutRender[0].components[0]).toHaveProperty("type", "Widgets");
});
it("should filter out Widgets when both widget prefs are false", () => {
const { layoutRender } = selectLayoutRender({
prefs: {
"widgets.system.enabled": false,
widgetsConfig: { enabled: false },
"feeds.section.topstories": false,
"feeds.system.topstories": false,
},
state: {
layout: [
{
width: 12,
components: [{ type: "Widgets" }],
},
],
},
});
expect(layoutRender).toHaveLength(0);
});
describe("spoc injection based on allowAds", () => {
const fakeLayout = [
{
width: 3,
components: [{ type: "CardGrid", feed: { url: "foo.com" } }],
},
];
const fakeSectionLayout = {
responsiveLayouts: [
{
columnCount: 1,
tiles: [
{ position: 0, hasAd: true },
{ position: 1, hasAd: false },
],
},
],
};
const fakeRecommendations = [
{ id: "rec1", section: "section-1", pos: 0 },
{ id: "rec2", section: "section-1", pos: 1 },
];
const fakeSpocs = {
lastUpdated: 0,
spocs: {
newtab_spocs: { items: [{ id: "spoc1", url: "https://spoc.com" }] },
},
};
const fakePrefs = {
"discoverystream.sections.enabled": true,
"feeds.section.topstories": true,
"feeds.system.topstories": true,
};
function setupStore(allowAds) {
store.dispatch({
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: fakeLayout },
});
store.dispatch({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: {
feed: {
data: {
recommendations: fakeRecommendations,
sections: [
{
sectionKey: "section-1",
allowAds,
receivedRank: 0,
layout: fakeSectionLayout,
},
],
},
},
url: "foo.com",
},
});
store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });
store.dispatch({
type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
data: fakeSpocs,
});
}
it("should not add spoc positions for sections with allowAds: false", () => {
setupStore(false);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: fakePrefs,
});
const [renderedSection] = layoutRender[0].components[0].data.sections;
expect(
renderedSection.data.filter(item => item.id === "spoc1")
).toHaveLength(0);
});
it("should add spoc positions for sections with allowAds: true", () => {
setupStore(true);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: fakePrefs,
});
const [renderedSection] = layoutRender[0].components[0].data.sections;
expect(renderedSection.data.some(item => item.id === "spoc1")).toBe(true);
});
it("should add spoc positions for sections with allowAds: undefined", () => {
setupStore(undefined);
const { layoutRender } = selectLayoutRender({
state: store.getState().DiscoveryStream,
prefs: fakePrefs,
});
const [renderedSection] = layoutRender[0].components[0].data.sections;
expect(renderedSection.data.some(item => item.id === "spoc1")).toBe(true);
});
});
});