Bug 1794020 - topsite promo tiles r=nanj

Differential Revision: https://phabricator.services.mozilla.com/D144438
This commit is contained in:
scott 2022-10-27 21:54:00 +00:00
Родитель 5de92f1759
Коммит ced6135bf5
9 изменённых файлов: 419 добавлений и 267 удалений

Просмотреть файл

@ -1582,6 +1582,8 @@ pref("browser.newtabpage.activity-stream.discoverystream.sponsored-collections.e
// Changes the spoc content.
pref("browser.newtabpage.activity-stream.discoverystream.spocAdTypes", "");
pref("browser.newtabpage.activity-stream.discoverystream.spocZoneIds", "");
pref("browser.newtabpage.activity-stream.discoverystream.spocTopsitesAdTypes", "");
pref("browser.newtabpage.activity-stream.discoverystream.spocTopsitesZoneIds", "");
pref("browser.newtabpage.activity-stream.discoverystream.spocSiteId", "");
pref("browser.newtabpage.activity-stream.discoverystream.sendToPocket.enabled", false);

Просмотреть файл

@ -35,6 +35,15 @@ add_task(async function test_firefox_home_without_policy_without_pocket() {
});
add_task(async function test_firefox_home_with_policy() {
await SpecialPowers.pushPrefEnv({
set: [
[
"browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear",
"",
],
],
});
await setupPolicyEngineWithJson({
policies: {
FirefoxHome: {
@ -65,6 +74,7 @@ add_task(async function test_firefox_home_with_policy() {
is(highlights, null, "Highlights section should not be there.");
});
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_firefoxhome_preferences_set() {

Просмотреть файл

@ -114,20 +114,15 @@ export class _DiscoveryStreamBase extends React.PureComponent {
case "Highlights":
return <Highlights />;
case "TopSites":
let promoAlignment;
if (
component.spocs &&
component.spocs.positions &&
component.spocs.positions.length
) {
promoAlignment =
component.spocs.positions[0].index === 0 ? "left" : "right";
let positions = [];
if (component?.spocs?.positions?.length) {
positions = component.spocs.positions;
}
return (
<TopSites
header={component.header}
data={component.data}
promoAlignment={promoAlignment}
promoPositions={positions}
/>
);
case "TextPromo":

Просмотреть файл

@ -4,7 +4,6 @@
import { connect } from "react-redux";
import { TopSites as OldTopSites } from "content-src/components/TopSites/TopSites";
import { TOP_SITES_MAX_SITES_PER_ROW } from "common/Reducers.jsm";
import React from "react";
export class _TopSites extends React.PureComponent {
@ -31,37 +30,8 @@ export class _TopSites extends React.PureComponent {
);
}
// Find the first empty or unpinned index we can place the SPOC in.
// Return -1 if no available index and we should push it at the end.
getFirstAvailableIndex(topSites, promoAlignment) {
if (promoAlignment === "left") {
return topSites.findIndex(topSite => !topSite || !topSite.isPinned);
}
// The row isn't full so we can push it to the end of the row.
if (topSites.length < TOP_SITES_MAX_SITES_PER_ROW) {
return -1;
}
// If the row is full, we can check the row first for unpinned topsites to replace.
// Else we can check after the row. This behavior is how unpinned topsites move while drag and drop.
let endOfRow = TOP_SITES_MAX_SITES_PER_ROW - 1;
for (let i = endOfRow; i >= 0; i--) {
if (!topSites[i] || !topSites[i].isPinned) {
return i;
}
}
for (let i = endOfRow + 1; i < topSites.length; i++) {
if (!topSites[i] || !topSites[i].isPinned) {
return i;
}
}
return -1;
}
insertSpocContent(TopSites, data, promoAlignment) {
// For the time being we only support 1 position.
insertSpocContent(TopSites, data, promoPosition) {
if (
!TopSites.rows ||
TopSites.rows.length === 0 ||
@ -91,51 +61,22 @@ export class _TopSites extends React.PureComponent {
// For now we are assuming position based on intended position.
// Actual position can shift based on other content.
// We also hard code left and right to be 0 and 7.
// We send the intended postion in the ping.
pos: promoAlignment === "left" ? 0 : 7,
// We send the intended position in the ping.
pos: promoPosition,
};
const firstAvailableIndex = this.getFirstAvailableIndex(
topSites,
promoAlignment
);
if (firstAvailableIndex === -1) {
topSites.push(link);
} else {
// Normal insertion will not work since pinned topsites are in their correct index already
// Similar logic is done to handle drag and drop with pinned topsites in TopSite.jsx
let shiftedTopSite = topSites[firstAvailableIndex];
let index = firstAvailableIndex + 1;
// Shift unpinned topsites to the right by finding the next unpinned topsite to replace
while (shiftedTopSite) {
if (index === topSites.length) {
topSites.push(shiftedTopSite);
shiftedTopSite = null;
} else if (topSites[index] && topSites[index].isPinned) {
index += 1;
} else {
const nextTopSite = topSites[index];
topSites[index] = shiftedTopSite;
shiftedTopSite = nextTopSite;
index += 1;
}
}
topSites[firstAvailableIndex] = link;
}
const replaceCount = topSites[promoPosition]?.show_sponsored_label ? 1 : 0;
topSites.splice(promoPosition, replaceCount, link);
return { ...TopSites, rows: topSites };
}
render() {
const { header = {}, data, promoAlignment, TopSites } = this.props;
const { header = {}, data, promoPositions, TopSites } = this.props;
const TopSitesWithSpoc =
TopSites && data && promoAlignment
? this.insertSpocContent(TopSites, data, promoAlignment)
TopSites && data && promoPositions?.length
? this.insertSpocContent(TopSites, data, promoPositions[0].index)
: null;
return (

Просмотреть файл

@ -13456,7 +13456,6 @@ const selectLayoutRender = ({
class TopSites_TopSites_TopSites extends (external_React_default()).PureComponent {
// Find a SPOC that doesn't already exist in User's TopSites
getFirstAvailableSpoc(topSites, data) {
@ -13472,40 +13471,12 @@ class TopSites_TopSites_TopSites extends (external_React_default()).PureComponen
// Spoc domains are in the format 'sponsorname.com'
return spocs.find(spoc => !userTopSites.has(spoc.url) && !userTopSites.has(`http://${spoc.domain}`) && !userTopSites.has(`https://${spoc.domain}`) && !userTopSites.has(`http://www.${spoc.domain}`) && !userTopSites.has(`https://www.${spoc.domain}`));
} // Find the first empty or unpinned index we can place the SPOC in.
// Return -1 if no available index and we should push it at the end.
} // For the time being we only support 1 position.
getFirstAvailableIndex(topSites, promoAlignment) {
if (promoAlignment === "left") {
return topSites.findIndex(topSite => !topSite || !topSite.isPinned);
} // The row isn't full so we can push it to the end of the row.
insertSpocContent(TopSites, data, promoPosition) {
var _topSites$promoPositi;
if (topSites.length < TOP_SITES_MAX_SITES_PER_ROW) {
return -1;
} // If the row is full, we can check the row first for unpinned topsites to replace.
// Else we can check after the row. This behavior is how unpinned topsites move while drag and drop.
let endOfRow = TOP_SITES_MAX_SITES_PER_ROW - 1;
for (let i = endOfRow; i >= 0; i--) {
if (!topSites[i] || !topSites[i].isPinned) {
return i;
}
}
for (let i = endOfRow + 1; i < topSites.length; i++) {
if (!topSites[i] || !topSites[i].isPinned) {
return i;
}
}
return -1;
}
insertSpocContent(TopSites, data, promoAlignment) {
if (!TopSites.rows || TopSites.rows.length === 0 || !data.spocs || data.spocs.length === 0) {
return null;
}
@ -13530,36 +13501,11 @@ class TopSites_TopSites_TopSites extends (external_React_default()).PureComponen
// For now we are assuming position based on intended position.
// Actual position can shift based on other content.
// We also hard code left and right to be 0 and 7.
// We send the intended postion in the ping.
pos: promoAlignment === "left" ? 0 : 7
// We send the intended position in the ping.
pos: promoPosition
};
const firstAvailableIndex = this.getFirstAvailableIndex(topSites, promoAlignment);
if (firstAvailableIndex === -1) {
topSites.push(link);
} else {
// Normal insertion will not work since pinned topsites are in their correct index already
// Similar logic is done to handle drag and drop with pinned topsites in TopSite.jsx
let shiftedTopSite = topSites[firstAvailableIndex];
let index = firstAvailableIndex + 1; // Shift unpinned topsites to the right by finding the next unpinned topsite to replace
while (shiftedTopSite) {
if (index === topSites.length) {
topSites.push(shiftedTopSite);
shiftedTopSite = null;
} else if (topSites[index] && topSites[index].isPinned) {
index += 1;
} else {
const nextTopSite = topSites[index];
topSites[index] = shiftedTopSite;
shiftedTopSite = nextTopSite;
index += 1;
}
}
topSites[firstAvailableIndex] = link;
}
const replaceCount = (_topSites$promoPositi = topSites[promoPosition]) !== null && _topSites$promoPositi !== void 0 && _topSites$promoPositi.show_sponsored_label ? 1 : 0;
topSites.splice(promoPosition, replaceCount, link);
return { ...TopSites,
rows: topSites
};
@ -13569,10 +13515,10 @@ class TopSites_TopSites_TopSites extends (external_React_default()).PureComponen
const {
header = {},
data,
promoAlignment,
promoPositions,
TopSites
} = this.props;
const TopSitesWithSpoc = TopSites && data && promoAlignment ? this.insertSpocContent(TopSites, data, promoAlignment) : null;
const TopSitesWithSpoc = TopSites && data && promoPositions !== null && promoPositions !== void 0 && promoPositions.length ? this.insertSpocContent(TopSites, data, promoPositions[0].index) : null;
return /*#__PURE__*/external_React_default().createElement("div", {
className: `ds-top-sites ${TopSitesWithSpoc ? "top-sites-spoc" : ""}`
}, /*#__PURE__*/external_React_default().createElement(TopSites_TopSites, {
@ -13679,21 +13625,23 @@ class _DiscoveryStreamBase extends (external_React_default()).PureComponent {
}
renderComponent(component, embedWidth) {
var _component$spocs, _component$spocs$posi;
switch (component.type) {
case "Highlights":
return /*#__PURE__*/external_React_default().createElement(Highlights, null);
case "TopSites":
let promoAlignment;
let positions = [];
if (component.spocs && component.spocs.positions && component.spocs.positions.length) {
promoAlignment = component.spocs.positions[0].index === 0 ? "left" : "right";
if (component !== null && component !== void 0 && (_component$spocs = component.spocs) !== null && _component$spocs !== void 0 && (_component$spocs$posi = _component$spocs.positions) !== null && _component$spocs$posi !== void 0 && _component$spocs$posi.length) {
positions = component.spocs.positions;
}
return /*#__PURE__*/external_React_default().createElement(DiscoveryStreamComponents_TopSites_TopSites_TopSites, {
header: component.header,
data: component.data,
promoAlignment: promoAlignment
promoPositions: positions
});
case "TextPromo":

Просмотреть файл

@ -55,8 +55,11 @@ const PREF_SPOCS_ENDPOINT_QUERY = "discoverystream.spocs-endpoint-query";
const PREF_REGION_BASIC_LAYOUT = "discoverystream.region-basic-layout";
const PREF_USER_TOPSTORIES = "feeds.section.topstories";
const PREF_SYSTEM_TOPSTORIES = "feeds.system.topstories";
const PREF_USER_TOPSITES = "feeds.topsites";
const PREF_SYSTEM_TOPSITES = "feeds.system.topsites";
const PREF_SPOCS_CLEAR_ENDPOINT = "discoverystream.endpointSpocsClear";
const PREF_SHOW_SPONSORED = "showSponsored";
const PREF_SHOW_SPONSORED_TOPSITES = "showSponsoredTopSites";
const PREF_SPOC_IMPRESSIONS = "discoverystream.spoc.impressions";
const PREF_FLIGHT_BLOCKS = "discoverystream.flight.blocks";
const PREF_REC_IMPRESSIONS = "discoverystream.rec.impressions";
@ -148,6 +151,12 @@ class DiscoveryStreamFeed {
}
get showSpocs() {
// High level overall sponsored check, if one of these is true,
// we know we need some sort of spoc control setup.
return this.showSponsoredStories || this.showSponsoredTopsites;
}
get showSponsoredStories() {
// Combine user-set sponsored opt-out with Mozilla-set config
return (
this.store.getState().Prefs.values[PREF_SHOW_SPONSORED] &&
@ -155,14 +164,27 @@ class DiscoveryStreamFeed {
);
}
get showStories() {
get showSponsoredTopsites() {
// Combine user-set sponsored opt-out with Mozilla-set config
return this.store.getState().Prefs.values[PREF_SHOW_SPONSORED_TOPSITES];
}
get showStories() {
// Combine user-set stories opt-out with Mozilla-set config
return (
this.store.getState().Prefs.values[PREF_SYSTEM_TOPSTORIES] &&
this.store.getState().Prefs.values[PREF_USER_TOPSTORIES]
);
}
get showTopsites() {
// Combine user-set topsites opt-out with Mozilla-set config
return (
this.store.getState().Prefs.values[PREF_SYSTEM_TOPSITES] &&
this.store.getState().Prefs.values[PREF_USER_TOPSITES]
);
}
get personalized() {
// If stories are not displayed, no point in trying to personalize them.
if (!this.showStories) {
@ -512,25 +534,48 @@ class DiscoveryStreamFeed {
const placements = [];
const placementsMap = {};
for (const row of layout.filter(r => r.components && r.components.length)) {
for (const component of row.components) {
if (component.placement) {
// Throw away any dupes for the request.
if (!placementsMap[component.placement.name]) {
placementsMap[component.placement.name] = component.placement;
placements.push(component.placement);
for (const component of row.components.filter(
c => c.placement && c.spocs
)) {
// If we find a valid placement, we set it to this value.
let placement;
// We need to check to see if this placement is on or not.
// If this placement has a prefs array, check against that.
if (component.spocs.prefs) {
// Check every pref in the array to see if this placement is turned on.
if (
component.spocs.prefs.length &&
component.spocs.prefs.every(
p => this.store.getState().Prefs.values[p]
)
) {
// This placement is on.
placement = component.placement;
}
} else if (this.showSponsoredStories) {
// If we do not have a prefs array, use old check.
// This is because Pocket spocs uses an old non pref method.
placement = component.placement;
}
// Validate this placement and check for dupes.
if (placement?.name && !placementsMap[placement.name]) {
placementsMap[placement.name] = placement;
placements.push(placement);
}
}
}
if (placements.length) {
sendUpdate({
type: at.DISCOVERY_STREAM_SPOCS_PLACEMENTS,
data: { placements },
meta: {
isStartup,
},
});
}
// Update placements data.
// Even if we have no placements, we still want to update it to clear it.
sendUpdate({
type: at.DISCOVERY_STREAM_SPOCS_PLACEMENTS,
data: { placements },
meta: {
isStartup,
},
});
}
/**
@ -602,16 +647,20 @@ class DiscoveryStreamFeed {
items = isBasicLayout ? 4 : 24;
}
const spocAdTypes = pocketConfig.spocAdTypes
?.split(",")
.filter(item => item)
.map(item => parseInt(item, 10));
const spocZoneIds = pocketConfig.spocZoneIds
?.split(",")
.filter(item => item)
.map(item => parseInt(item, 10));
const prepConfArr = arr => {
return arr
?.split(",")
.filter(item => item)
.map(item => parseInt(item, 10));
};
const spocAdTypes = prepConfArr(pocketConfig.spocAdTypes);
const spocZoneIds = prepConfArr(pocketConfig.spocZoneIds);
const spocTopsitesAdTypes = prepConfArr(pocketConfig.spocTopsitesAdTypes);
const spocTopsitesZoneIds = prepConfArr(pocketConfig.spocTopsitesZoneIds);
const { spocSiteId } = pocketConfig;
let spocPlacementData;
let spocTopsitesPlacementData;
let spocsUrl;
if (spocAdTypes?.length && spocZoneIds?.length) {
@ -621,6 +670,13 @@ class DiscoveryStreamFeed {
};
}
if (spocTopsitesAdTypes?.length && spocTopsitesZoneIds?.length) {
spocTopsitesPlacementData = {
ad_types: spocTopsitesAdTypes,
zone_ids: spocTopsitesZoneIds,
};
}
if (spocSiteId) {
const newUrl = new URL(SPOCS_URL);
newUrl.searchParams.set("site", spocSiteId);
@ -634,6 +690,7 @@ class DiscoveryStreamFeed {
items,
sponsoredCollectionsEnabled,
spocPlacementData,
spocTopsitesPlacementData,
spocPositions: this.parseGridPositions(
pocketConfig.spocPositions?.split(`,`)
),
@ -835,10 +892,6 @@ class DiscoveryStreamFeed {
getPlacements() {
const { placements } = this.store.getState().DiscoveryStream.spocs;
// Backwards comp for before we had placements, assume just a single spocs placement.
if (!placements || !placements.length) {
return [{ name: "spocs" }];
}
return placements;
}
@ -927,9 +980,9 @@ class DiscoveryStreamFeed {
const cachedData = (await this.cache.get()) || {};
let spocsState;
const { placements } = this.store.getState().DiscoveryStream.spocs;
const placements = this.getPlacements();
if (this.showSpocs) {
if (this.showSpocs && placements?.length) {
spocsState = cachedData.spocs;
if (this.isExpired({ cachedData, key: "spocs", isStartup })) {
const endpoint = this.store.getState().DiscoveryStream.spocs
@ -1557,15 +1610,25 @@ class DiscoveryStreamFeed {
: this.store.dispatch;
await this.loadLayout(dispatch, isStartup);
if (this.showStories) {
await Promise.all([
this.loadSpocs(dispatch, isStartup).catch(error =>
Cu.reportError(`Error trying to load spocs feeds: ${error}`)
),
this.loadComponentFeeds(dispatch, isStartup).catch(error =>
if (this.showStories || this.showTopsites) {
const promises = [];
// We could potentially have either or both sponsored topsites or stories.
// We only make one fetch, and control which to request when we fetch.
// So for now we only care if we need to make this request at all.
const spocsPromise = this.loadSpocs(dispatch, isStartup).catch(error =>
Cu.reportError(`Error trying to load spocs feeds: ${error}`)
);
promises.push(spocsPromise);
if (this.showStories) {
const storiesPromise = this.loadComponentFeeds(
dispatch,
isStartup
).catch(error =>
Cu.reportError(`Error trying to load component feeds: ${error}`)
),
]);
);
promises.push(storiesPromise);
}
await Promise.all(promises);
if (isStartup) {
await this._maybeUpdateCachedData();
}
@ -1792,6 +1855,8 @@ class DiscoveryStreamFeed {
break;
case PREF_USER_TOPSTORIES:
case PREF_SYSTEM_TOPSTORIES:
case PREF_USER_TOPSITES:
case PREF_SYSTEM_TOPSITES:
if (!action.data.value) {
// Ensure we delete any remote data potentially related to spocs.
this.clearSpocs();
@ -1801,13 +1866,21 @@ class DiscoveryStreamFeed {
break;
// Check if spocs was disabled. Remove them if they were.
case PREF_SHOW_SPONSORED:
case PREF_SHOW_SPONSORED_TOPSITES:
if (!action.data.value) {
// Ensure we delete any remote data potentially related to spocs.
this.clearSpocs();
}
await this.loadSpocs(update =>
this.store.dispatch(ac.BroadcastToContent(update))
const dispatch = update =>
this.store.dispatch(ac.BroadcastToContent(update));
// We refresh placements data because one of the spocs were turned off.
this.updatePlacements(
dispatch,
this.store.getState().DiscoveryStream.layout
);
// Placements have changed so consider spocs expired, and reload them.
await this.cache.set("spocs", {});
await this.loadSpocs(dispatch);
break;
}
}
@ -2021,7 +2094,6 @@ class DiscoveryStreamFeed {
case at.PREF_CHANGED:
await this.onPrefChangedAction(action);
if (action.data.name === "pocketConfig") {
// TODO Now that this is happening for real, do we need the placement changed function moved.
await this.onPrefChange();
this.setupPrefs(false /* isStartup */);
}
@ -2039,6 +2111,7 @@ class DiscoveryStreamFeed {
`items` How many items to include in the primary card grid.
`spocPositions` Changes the position of spoc cards.
`spocPlacementData` Used to set the spoc content.
`spocTopsitesPlacementData` Used to set spoc content for topsites.
`sponsoredCollectionsEnabled` Tuns on and off the sponsored collection section.
`hybridLayout` Changes cards to smaller more compact cards only for specific breakpoints.
`hideCardBackground` Removes Pocket card background and borders.
@ -2053,6 +2126,7 @@ getHardcodedLayout = ({
items = 21,
spocPositions = [1, 5, 7, 11, 18, 20],
spocPlacementData = { ad_types: [3617], zone_ids: [217758, 217995] },
spocTopsitesPlacementData,
widgetPositions = [],
widgetData = [],
sponsoredCollectionsEnabled = false,
@ -2079,6 +2153,24 @@ getHardcodedLayout = ({
id: "newtab-section-header-topsites",
},
},
...(spocTopsitesPlacementData
? {
placement: {
name: "sponsored-topsites",
ad_types: spocTopsitesPlacementData.ad_types,
zone_ids: spocTopsitesPlacementData.zone_ids,
},
spocs: {
probability: 1,
prefs: [PREF_SHOW_SPONSORED_TOPSITES],
positions: [
{
index: 1,
},
],
},
}
: {}),
properties: {},
},
...(sponsoredCollectionsEnabled

Просмотреть файл

@ -80,7 +80,7 @@ describe("Discovery Stream <TopSites>", () => {
shim: { impression: "1011" },
};
const data = { spocs: [topSiteSpoc] };
const resultSpocLeft = {
const resultSpocFirst = {
customScreenshotURL: "foobar",
type: "SPOC",
label: "bar",
@ -94,7 +94,7 @@ describe("Discovery Stream <TopSites>", () => {
},
pos: 0,
};
const resultSpocRight = {
const resultSpocForth = {
customScreenshotURL: "foobar",
type: "SPOC",
label: "bar",
@ -106,7 +106,7 @@ describe("Discovery Stream <TopSites>", () => {
shim: {
impression: "1011",
},
pos: 7,
pos: 4,
};
const pinnedSite = {
label: "pinnedSite",
@ -119,8 +119,8 @@ describe("Discovery Stream <TopSites>", () => {
});
it("Should return null if no data or no TopSites", () => {
assert.isNull(insertSpocContent(defaultTopSites, {}, "right"));
assert.isNull(insertSpocContent({}, data, "right"));
assert.isNull(insertSpocContent(defaultTopSites, {}, 1));
assert.isNull(insertSpocContent({}, data, 1));
});
it("Should return null if an organic SPOC topsite exists", () => {
@ -128,7 +128,7 @@ describe("Discovery Stream <TopSites>", () => {
rows: [...defaultTopSiteRows, topSiteSpoc],
};
assert.isNull(insertSpocContent(topSitesWithOrganicSpoc, data, "right"));
assert.isNull(insertSpocContent(topSitesWithOrganicSpoc, data, 1));
});
it("Should return next spoc if the first SPOC is an existing organic top site", () => {
@ -152,7 +152,7 @@ describe("Discovery Stream <TopSites>", () => {
const result = insertSpocContent(
topSitesWithOrganicSpoc,
extraSpocData,
"right"
5
);
const availableSpoc = {
@ -167,7 +167,7 @@ describe("Discovery Stream <TopSites>", () => {
shim: {
impression: "1011",
},
pos: 7,
pos: 5,
};
const expectedResult = {
rows: [...topSitesWithOrganicSpoc.rows, availableSpoc],
@ -176,71 +176,33 @@ describe("Discovery Stream <TopSites>", () => {
assert.deepEqual(result, expectedResult);
});
it("should add to end of row if the row is not full and alignment is right", () => {
const result = insertSpocContent(defaultTopSites, data, "right");
it("should add spoc to the 4th position", () => {
const result = insertSpocContent(defaultTopSites, data, 4);
const expectedResult = {
rows: [...defaultTopSiteRows, resultSpocRight],
rows: [...defaultTopSiteRows, resultSpocForth],
};
assert.deepEqual(result, expectedResult);
});
it("should add to front of row if the row is not full and alignment is left", () => {
const result = insertSpocContent(defaultTopSites, data, "left");
it("should add to first position", () => {
const result = insertSpocContent(defaultTopSites, data, 0);
assert.deepEqual(result, {
rows: [resultSpocLeft, ...defaultTopSiteRows],
rows: [resultSpocFirst, ...defaultTopSiteRows],
});
});
it("should add to first available in the front row if alignment is left and there are pins", () => {
it("should add to first position even if there are pins", () => {
const topSiteRowsWithPins = [
pinnedSite,
pinnedSite,
...defaultTopSiteRows,
];
const result = insertSpocContent(
{ rows: topSiteRowsWithPins },
data,
"left"
);
const result = insertSpocContent({ rows: topSiteRowsWithPins }, data, 0);
assert.deepEqual(result, {
rows: [pinnedSite, pinnedSite, resultSpocLeft, ...defaultTopSiteRows],
});
});
it("should add to first available in the next row if alignment is right and there are all pins in the front row", () => {
const pinnedArray = new Array(8).fill(pinnedSite);
const result = insertSpocContent({ rows: pinnedArray }, data, "right");
assert.deepEqual(result, {
rows: [...pinnedArray, resultSpocRight],
});
});
it("should add to first available in the current row if alignment is right and there are some pins in the front row", () => {
const pinnedArray = new Array(6).fill(pinnedSite);
const topSite = { label: "foo" };
const rowsWithPins = [topSite, topSite, ...pinnedArray];
const result = insertSpocContent({ rows: rowsWithPins }, data, "right");
assert.deepEqual(result, {
rows: [topSite, resultSpocRight, ...pinnedArray, topSite],
});
});
it("should preserve the indices of pinned items", () => {
const topSite = { label: "foo" };
const rowsWithPins = [pinnedSite, topSite, topSite, pinnedSite];
const result = insertSpocContent({ rows: rowsWithPins }, data, "left");
// Pinned items should retain in Index 0 and Index 3 like defined in rowsWithPins
assert.deepEqual(result, {
rows: [pinnedSite, resultSpocLeft, topSite, pinnedSite, topSite],
rows: [resultSpocFirst, pinnedSite, pinnedSite, ...defaultTopSiteRows],
});
});
});

Просмотреть файл

@ -585,6 +585,28 @@ describe("DiscoveryStreamFeed", () => {
7890,
]);
});
it("should create a layout with spoc topsite position data", async () => {
feed.config.hardcoded_layout = true;
feed.store = createStore(combineReducers(reducers), {
Prefs: {
values: {
pocketConfig: {
spocTopsitesAdTypes: "1230",
spocTopsitesZoneIds: "4560, 7890",
},
},
},
});
await feed.loadLayout(feed.store.dispatch);
const { layout } = feed.store.getState().DiscoveryStream;
assert.deepEqual(layout[0].components[0].placement.ad_types, [1230]);
assert.deepEqual(layout[0].components[0].placement.zone_ids, [
4560,
7890,
]);
});
it("should create a layout with proper spoc url with a site id", async () => {
feed.config.hardcoded_layout = true;
feed.store = createStore(combineReducers(reducers), {
@ -607,12 +629,18 @@ describe("DiscoveryStreamFeed", () => {
});
describe("#updatePlacements", () => {
it("should fire update placements without dupes with updatePlacements", () => {
it("should dispatch DISCOVERY_STREAM_SPOCS_PLACEMENTS", () => {
sandbox.spy(feed.store, "dispatch");
feed.store.getState = () => ({
Prefs: { values: { showSponsored: true } },
});
Object.defineProperty(feed, "config", {
get: () => ({ show_spocs: true }),
});
const fakeComponents = {
components: [
{ placement: { name: "first" } },
{ placement: { name: "second" } },
{ placement: { name: "first" }, spocs: {} },
{ placement: { name: "second" }, spocs: {} },
],
};
const fakeLayout = [fakeComponents];
@ -626,6 +654,36 @@ describe("DiscoveryStreamFeed", () => {
meta: { isStartup: false },
});
});
it("should dispatch DISCOVERY_STREAM_SPOCS_PLACEMENTS with prefs array", () => {
sandbox.spy(feed.store, "dispatch");
feed.store.getState = () => ({
Prefs: { values: { showSponsored: true, withPref: true } },
});
Object.defineProperty(feed, "config", {
get: () => ({ show_spocs: true }),
});
const fakeComponents = {
components: [
{ placement: { name: "withPref" }, spocs: { prefs: ["withPref"] } },
{ placement: { name: "withoutPref1" }, spocs: {} },
{
placement: { name: "withoutPref2" },
spocs: { prefs: ["whatever"] },
},
{ placement: { name: "withoutPref3" }, spocs: { prefs: [] } },
],
};
const fakeLayout = [fakeComponents];
feed.updatePlacements(feed.store.dispatch, fakeLayout);
assert.calledOnce(feed.store.dispatch);
assert.calledWith(feed.store.dispatch, {
type: "DISCOVERY_STREAM_SPOCS_PLACEMENTS",
data: { placements: [{ name: "withPref" }, { name: "withoutPref1" }] },
meta: { isStartup: false },
});
});
it("should fire update placements from loadLayout", async () => {
sandbox.spy(feed, "updatePlacements");
@ -637,27 +695,20 @@ describe("DiscoveryStreamFeed", () => {
describe("#placementsForEach", () => {
it("should forEach through placements", () => {
const fakeComponents = {
components: [
{ placement: { name: "first" } },
{ placement: { name: "second" } },
],
};
const fakeLayout = [fakeComponents];
feed.updatePlacements(feed.store.dispatch, fakeLayout);
feed.store.getState = () => ({
DiscoveryStream: {
spocs: {
placements: [{ name: "first" }, { name: "second" }],
},
},
});
let items = [];
feed.placementsForEach(item => items.push(item.name));
assert.deepEqual(items, ["first", "second"]);
});
it("should forEach through placements for just spocs if no placements exist", () => {
let items = [];
feed.placementsForEach(item => items.push(item.name));
assert.deepEqual(items, ["spocs"]);
});
});
describe("#loadLayoutEndPointUsingPref", () => {
@ -958,6 +1009,8 @@ describe("DiscoveryStreamFeed", () => {
api_key_pref: "",
},
};
sandbox.stub(feed, "getPlacements").returns([{ name: "spocs" }]);
Object.defineProperty(feed, "showSpocs", { get: () => true });
});
it("should not fetch or update cache if no spocs endpoint is defined", async () => {
@ -1081,6 +1134,12 @@ describe("DiscoveryStreamFeed", () => {
assert.calledOnce(feed.personalizationOverride);
});
it("should return expected data if normalizeSpocsItems returns no spoc data", async () => {
// We don't need this for just this test, we are setting placements manually.
feed.getPlacements.restore();
Object.defineProperty(feed, "showSponsoredStories", {
get: () => true,
});
sandbox.stub(feed.cache, "get").returns(Promise.resolve());
sandbox
.stub(feed, "fetchFromEndpoint")
@ -1089,8 +1148,8 @@ describe("DiscoveryStreamFeed", () => {
const fakeComponents = {
components: [
{ placement: { name: "placement1" } },
{ placement: { name: "placement2" } },
{ placement: { name: "placement1" }, spocs: {} },
{ placement: { name: "placement2" }, spocs: {} },
],
};
feed.updatePlacements(feed.store.dispatch, [fakeComponents]);
@ -1113,6 +1172,11 @@ describe("DiscoveryStreamFeed", () => {
});
});
it("should use title and context on spoc data", async () => {
// We don't need this for just this test, we are setting placements manually.
feed.getPlacements.restore();
Object.defineProperty(feed, "showSponsoredStories", {
get: () => true,
});
sandbox.stub(feed.cache, "get").returns(Promise.resolve());
sandbox.stub(feed, "fetchFromEndpoint").resolves({
placement1: {
@ -1126,7 +1190,7 @@ describe("DiscoveryStreamFeed", () => {
sandbox.stub(feed.cache, "set").returns(Promise.resolve());
const fakeComponents = {
components: [{ placement: { name: "placement1" } }],
components: [{ placement: { name: "placement1" }, spocs: {} }],
};
feed.updatePlacements(feed.store.dispatch, [fakeComponents]);
@ -1183,7 +1247,46 @@ describe("DiscoveryStreamFeed", () => {
});
describe("#showSpocs", () => {
it("should return false from showSpocs if user pref showSponsored is false", async () => {
it("should return true from showSpocs if showSponsoredStories is false", async () => {
Object.defineProperty(feed, "showSponsoredStories", {
get: () => false,
});
Object.defineProperty(feed, "showSponsoredTopsites", {
get: () => true,
});
assert.isTrue(feed.showSpocs);
});
it("should return true from showSpocs if showSponsoredTopsites is false", async () => {
Object.defineProperty(feed, "showSponsoredStories", {
get: () => true,
});
Object.defineProperty(feed, "showSponsoredTopsites", {
get: () => false,
});
assert.isTrue(feed.showSpocs);
});
it("should return true from showSpocs if both are true", async () => {
Object.defineProperty(feed, "showSponsoredStories", {
get: () => true,
});
Object.defineProperty(feed, "showSponsoredTopsites", {
get: () => true,
});
assert.isTrue(feed.showSpocs);
});
it("should return false from showSpocs if both are false", async () => {
Object.defineProperty(feed, "showSponsoredStories", {
get: () => false,
});
Object.defineProperty(feed, "showSponsoredTopsites", {
get: () => false,
});
assert.isFalse(feed.showSpocs);
});
});
describe("#showSponsoredStories", () => {
it("should return false from showSponsoredStories if user pref showSponsored is false", async () => {
feed.store.getState = () => ({
Prefs: { values: { showSponsored: false } },
});
@ -1191,9 +1294,9 @@ describe("DiscoveryStreamFeed", () => {
get: () => ({ show_spocs: true }),
});
assert.isFalse(feed.showSpocs);
assert.isFalse(feed.showSponsoredStories);
});
it("should return false from showSpocs if DiscoveryStream pref show_spocs is false", async () => {
it("should return false from showSponsoredStories if DiscoveryStream pref show_spocs is false", async () => {
feed.store.getState = () => ({
Prefs: { values: { showSponsored: true } },
});
@ -1201,9 +1304,9 @@ describe("DiscoveryStreamFeed", () => {
get: () => ({ show_spocs: false }),
});
assert.isFalse(feed.showSpocs);
assert.isFalse(feed.showSponsoredStories);
});
it("should return true from showSpocs if both prefs are true", async () => {
it("should return true from showSponsoredStories if both prefs are true", async () => {
feed.store.getState = () => ({
Prefs: { values: { showSponsored: true } },
});
@ -1211,7 +1314,94 @@ describe("DiscoveryStreamFeed", () => {
get: () => ({ show_spocs: true }),
});
assert.isTrue(feed.showSpocs);
assert.isTrue(feed.showSponsoredStories);
});
});
describe("#showSponsoredTopsites", () => {
it("should return false from showSponsoredTopsites if user pref showSponsoredTopSites is false", async () => {
feed.store.getState = () => ({
Prefs: { values: { showSponsoredTopSites: false } },
});
assert.isFalse(feed.showSponsoredTopsites);
});
it("should return true from showSponsoredTopsites if user pref showSponsoredTopSites is true", async () => {
feed.store.getState = () => ({
Prefs: { values: { showSponsoredTopSites: true } },
});
assert.isTrue(feed.showSponsoredTopsites);
});
});
describe("#showStories", () => {
it("should return false from showStories if user pref is false", async () => {
feed.store.getState = () => ({
Prefs: {
values: {
"feeds.section.topstories": false,
"feeds.system.topstories": true,
},
},
});
assert.isFalse(feed.showStories);
});
it("should return false from showStories if system pref is false", async () => {
feed.store.getState = () => ({
Prefs: {
values: {
"feeds.section.topstories": true,
"feeds.system.topstories": false,
},
},
});
assert.isFalse(feed.showStories);
});
it("should return true from showStories if both prefs are true", async () => {
feed.store.getState = () => ({
Prefs: {
values: {
"feeds.section.topstories": true,
"feeds.system.topstories": true,
},
},
});
assert.isTrue(feed.showStories);
});
});
describe("#showTopsites", () => {
it("should return false from showTopsites if user pref is false", async () => {
feed.store.getState = () => ({
Prefs: {
values: {
"feeds.topsites": false,
"feeds.system.topsites": true,
},
},
});
assert.isFalse(feed.showTopsites);
});
it("should return false from showTopsites if system pref is false", async () => {
feed.store.getState = () => ({
Prefs: {
values: {
"feeds.topsites": true,
"feeds.system.topsites": false,
},
},
});
assert.isFalse(feed.showTopsites);
});
it("should return true from showTopsites if both prefs are true", async () => {
feed.store.getState = () => ({
Prefs: {
values: {
"feeds.topsites": true,
"feeds.system.topsites": true,
},
},
});
assert.isTrue(feed.showTopsites);
});
});
@ -1696,6 +1886,7 @@ describe("DiscoveryStreamFeed", () => {
"flight-2": [Date.now() - 1],
"flight-3": [Date.now() - 1],
};
sandbox.stub(feed, "getPlacements").returns([{ name: "spocs" }]);
sandbox.stub(feed, "readDataPref").returns(fakeImpressions);
sandbox.stub(feed, "writeDataPref").returns();
@ -1946,6 +2137,7 @@ describe("DiscoveryStreamFeed", () => {
});
it("should call dispatch to ac.AlsoToPreloaded with filtered spoc data", async () => {
sandbox.stub(feed, "getPlacements").returns([{ name: "spocs" }]);
Object.defineProperty(feed, "showSpocs", { get: () => true });
const fakeImpressions = {
seen: [Date.now() - 1],
@ -1982,6 +2174,7 @@ describe("DiscoveryStreamFeed", () => {
);
});
it("should not call dispatch to ac.AlsoToPreloaded if spocs were not changed by frequency capping", async () => {
sandbox.stub(feed, "getPlacements").returns([{ name: "spocs" }]);
Object.defineProperty(feed, "showSpocs", { get: () => true });
const fakeImpressions = {};
sandbox.stub(feed, "recordFlightImpression").returns();
@ -2774,6 +2967,7 @@ describe("DiscoveryStreamFeed", () => {
});
it("should refresh spocs on startup if it was served from cache", async () => {
feed.loadSpocs.restore();
sandbox.stub(feed, "getPlacements").returns([{ name: "spocs" }]);
sandbox
.stub(feed.cache, "get")
.resolves({ spocs: { lastUpdated: Date.now() } });

Просмотреть файл

@ -293,6 +293,14 @@ pocketNewtab:
type: string
fallbackPref: browser.newtabpage.activity-stream.discoverystream.spocZoneIds
description: CSV string of data to set the spoc content.
spocTopsitesAdTypes:
type: string
fallbackPref: browser.newtabpage.activity-stream.discoverystream.spocTopsitesAdTypes
description: CSV string of data to set the spoc content.
spocTopsitesZoneIds:
type: string
fallbackPref: browser.newtabpage.activity-stream.discoverystream.spocTopsitesZoneIds
description: CSV string of data to set the spoc content.
spocSiteId:
type: string
fallbackPref: browser.newtabpage.activity-stream.discoverystream.spocSiteId