Bug 1386314 - Add default icons, enable snippets and bug fixes to Activity Stream. r=dmose

MozReview-Commit-ID: 9Pty4jAV6te

--HG--
extra : rebase_source : 62ea46ef7f892199dbaac8a333ce1bc8bd41ec22
This commit is contained in:
Ed Lee 2017-08-01 22:24:42 -07:00
Родитель 03b095bc52
Коммит e0da262e90
41 изменённых файлов: 809 добавлений и 228 удалений

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

@ -101,7 +101,7 @@ function onBrowserReady() {
waitingForBrowserReady = false;
// Listen for changes to the pref that enables Activity Stream
Services.prefs.addObserver(ACTIVITY_STREAM_ENABLED_PREF, observe);
Services.prefs.addObserver(ACTIVITY_STREAM_ENABLED_PREF, observe); // eslint-disable-line no-use-before-define
// Only initialize if the pref is true
if (Services.prefs.getBoolPref(ACTIVITY_STREAM_ENABLED_PREF, false)) {
@ -120,7 +120,7 @@ function observe(subject, topic, data) {
Services.tm.dispatchToMainThread(() => onBrowserReady());
break;
case PREF_CHANGED_TOPIC:
if (data == ACTIVITY_STREAM_ENABLED_PREF) {
if (data === ACTIVITY_STREAM_ENABLED_PREF) {
onPrefChanged();
}
break;

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

@ -0,0 +1,40 @@
this.Dedupe = class Dedupe {
constructor(createKey, compare) {
this.createKey = createKey || this.defaultCreateKey;
this.compare = compare || this.defaultCompare;
}
defaultCreateKey(item) {
return item;
}
defaultCompare() {
return false;
}
/**
* Dedupe an array containing groups of elements.
* Duplicate removal favors earlier groups.
*
* @param {Array} groups Contains an arbitrary number of arrays of elements.
* @returns {Array}
*/
group(groups) {
const globalKeys = new Set();
const result = [];
for (const values of groups) {
const valueMap = new Map();
for (const value of values) {
const key = this.createKey(value);
if (!globalKeys.has(key) && (!valueMap.has(key) || this.compare(valueMap.get(key), value))) {
valueMap.set(key, value);
}
}
result.push(valueMap);
valueMap.forEach((value, key) => globalKeys.add(key));
}
return result.map(m => Array.from(m.values()));
}
};
this.EXPORTED_SYMBOLS = ["Dedupe"];

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

@ -37,7 +37,7 @@ const INITIAL_STATE = {
function App(prevState = INITIAL_STATE.App, action) {
switch (action.type) {
case at.INIT:
return Object.assign({}, action.data || {}, {initialized: true});
return Object.assign({}, prevState, action.data || {}, {initialized: true});
case at.LOCALE_UPDATED: {
if (!action.data) {
return prevState;

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

@ -0,0 +1,32 @@
Components.utils.importGlobalProperties(["URL"]);
/**
* shortURL - Creates a short version of a link's url, used for display purposes
* e.g. {url: http://www.foosite.com, eTLD: "com"} => "foosite"
*
* @param {obj} link A link object
* {str} link.url (required)- The url of the link
* {str} link.eTLD (required) - The tld of the link
* e.g. for https://foo.org, the tld would be "org"
* Note that this property is added in various queries for ActivityStream
* via Services.eTLD.getPublicSuffix
* {str} link.hostname (optional) - The hostname of the url
* e.g. for http://www.hello.com/foo/bar, the hostname would be "www.hello.com"
* {str} link.title (optional) - The title of the link
* @return {str} A short url
*/
this.shortURL = function shortURL(link) {
if (!link.url && !link.hostname) {
return "";
}
const {eTLD} = link;
const hostname = (link.hostname || new URL(link.url).hostname).replace(/^www\./i, "");
// Remove the eTLD (e.g., com, net) and the preceding period from the hostname
const eTLDLength = (eTLD || "").length || (hostname.match(/\.com$/) && 3);
const eTLDExtra = eTLDLength > 0 ? -(eTLDLength + 1) : Infinity;
// If URL and hostname are not present fallback to page title.
return hostname.slice(0, eTLDExtra).toLowerCase() || hostname || link.title || link.url;
};
this.EXPORTED_SYMBOLS = ["shortURL"];

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

@ -63,7 +63,7 @@
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 26);
/******/ return __webpack_require__(__webpack_require__.s = 25);
/******/ })
/************************************************************************/
/******/ ([
@ -300,56 +300,19 @@ module.exports = ReactRedux;
"use strict";
/**
* shortURL - Creates a short version of a link's url, used for display purposes
* e.g. {url: http://www.foosite.com, eTLD: "com"} => "foosite"
*
* @param {obj} link A link object
* {str} link.url (required)- The url of the link
* {str} link.eTLD (required) - The tld of the link
* e.g. for https://foo.org, the tld would be "org"
* Note that this property is added in various queries for ActivityStream
* via Services.eTLD.getPublicSuffix
* {str} link.hostname (optional) - The hostname of the url
* e.g. for http://www.hello.com/foo/bar, the hostname would be "www.hello.com"
* {str} link.title (optional) - The title of the link
* @return {str} A short url
*/
module.exports = function shortURL(link) {
if (!link.url && !link.hostname) {
return "";
}
const eTLD = link.eTLD;
const hostname = (link.hostname || new URL(link.url).hostname).replace(/^www\./i, "");
// Remove the eTLD (e.g., com, net) and the preceding period from the hostname
const eTLDLength = (eTLD || "").length || hostname.match(/\.com$/) && 3;
const eTLDExtra = eTLDLength > 0 ? -(eTLDLength + 1) : Infinity;
// If URL and hostname are not present fallback to page title.
return hostname.slice(0, eTLDExtra).toLowerCase() || hostname || link.title || link.url;
};
/***/ }),
/* 5 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
const React = __webpack_require__(0);
var _require = __webpack_require__(2);
const injectIntl = _require.injectIntl;
const ContextMenu = __webpack_require__(16);
const ContextMenu = __webpack_require__(15);
var _require2 = __webpack_require__(1);
const ac = _require2.actionCreators;
const linkMenuOptions = __webpack_require__(23);
const linkMenuOptions = __webpack_require__(22);
const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow"];
class LinkMenu extends React.Component {
@ -404,7 +367,7 @@ module.exports = injectIntl(LinkMenu);
module.exports._unconnected = LinkMenu;
/***/ }),
/* 6 */
/* 5 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -527,7 +490,7 @@ module.exports = {
};
/***/ }),
/* 7 */
/* 6 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -544,12 +507,12 @@ var _require2 = __webpack_require__(2);
const addLocaleData = _require2.addLocaleData,
IntlProvider = _require2.IntlProvider;
const TopSites = __webpack_require__(21);
const Search = __webpack_require__(19);
const ConfirmDialog = __webpack_require__(15);
const ManualMigration = __webpack_require__(17);
const PreferencesPane = __webpack_require__(18);
const Sections = __webpack_require__(20);
const TopSites = __webpack_require__(20);
const Search = __webpack_require__(18);
const ConfirmDialog = __webpack_require__(14);
const ManualMigration = __webpack_require__(16);
const PreferencesPane = __webpack_require__(17);
const Sections = __webpack_require__(19);
// Locales that should be displayed RTL
const RTL_LIST = ["ar", "he", "fa", "ur"];
@ -573,7 +536,8 @@ class Base extends React.Component {
componentWillUpdate(_ref2) {
let App = _ref2.App;
if (App.locale !== this.props.App.locale) {
// Early loads might not have locale yet, so wait until we do
if (App.locale && App.locale !== this.props.App.locale) {
addLocaleDataForReactIntl(App);
this.updateTitle(App);
}
@ -621,7 +585,7 @@ class Base extends React.Component {
module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base);
/***/ }),
/* 8 */
/* 7 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -631,7 +595,7 @@ var _require = __webpack_require__(1);
const at = _require.actionTypes;
var _require2 = __webpack_require__(6);
var _require2 = __webpack_require__(5);
const perfSvc = _require2.perfService;
@ -701,7 +665,7 @@ module.exports = class DetectUserSessionStart {
};
/***/ }),
/* 9 */
/* 8 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -709,7 +673,7 @@ module.exports = class DetectUserSessionStart {
/* eslint-env mozilla/frame-script */
var _require = __webpack_require__(25);
var _require = __webpack_require__(24);
const createStore = _require.createStore,
combineReducers = _require.combineReducers,
@ -786,7 +750,7 @@ module.exports.OUTGOING_MESSAGE_NAME = OUTGOING_MESSAGE_NAME;
module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
/***/ }),
/* 10 */
/* 9 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -1097,10 +1061,10 @@ module.exports = {
SnippetsProvider,
SNIPPETS_UPDATE_INTERVAL_MS
};
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(24)))
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(23)))
/***/ }),
/* 11 */
/* 10 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -1149,7 +1113,7 @@ function App() {
switch (action.type) {
case at.INIT:
return Object.assign({}, action.data || {}, { initialized: true });
return Object.assign({}, prevState, action.data || {}, { initialized: true });
case at.LOCALE_UPDATED:
{
if (!action.data) {
@ -1401,27 +1365,26 @@ module.exports = {
};
/***/ }),
/* 12 */
/* 11 */
/***/ (function(module, exports) {
module.exports = ReactDOM;
/***/ }),
/* 13 */
/* 12 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
const React = __webpack_require__(0);
const LinkMenu = __webpack_require__(5);
const shortURL = __webpack_require__(4);
const LinkMenu = __webpack_require__(4);
var _require = __webpack_require__(2);
const FormattedMessage = _require.FormattedMessage;
const cardContextTypes = __webpack_require__(14);
const cardContextTypes = __webpack_require__(13);
var _require2 = __webpack_require__(1);
@ -1483,7 +1446,6 @@ class Card extends React.Component {
eventSource = _props.eventSource;
const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
const hostname = shortURL(link);
var _cardContextTypes$lin = cardContextTypes[link.type];
const icon = _cardContextTypes$lin.icon,
intlID = _cardContextTypes$lin.intlID;
@ -1506,7 +1468,7 @@ class Card extends React.Component {
"div",
{ className: "card-host-name" },
" ",
hostname,
link.hostname,
" "
),
React.createElement(
@ -1564,7 +1526,7 @@ class Card extends React.Component {
module.exports = Card;
/***/ }),
/* 14 */
/* 13 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -1590,7 +1552,7 @@ module.exports = {
};
/***/ }),
/* 15 */
/* 14 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -1709,7 +1671,7 @@ module.exports._unconnected = ConfirmDialog;
module.exports.Dialog = ConfirmDialog;
/***/ }),
/* 16 */
/* 15 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -1803,7 +1765,7 @@ module.exports.ContextMenu = ContextMenu;
module.exports.ContextMenuItem = ContextMenuItem;
/***/ }),
/* 17 */
/* 16 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -1881,7 +1843,7 @@ module.exports = connect()(ManualMigration);
module.exports._unconnected = ManualMigration;
/***/ }),
/* 18 */
/* 17 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -1928,10 +1890,13 @@ class PreferencesPane extends React.Component {
this.togglePane = this.togglePane.bind(this);
// TODO This is temporary until sections register their PreferenceInput component automatically
try {
this.topStoriesOptions = JSON.parse(props.Prefs.values["feeds.section.topstories.options"]);
} catch (e) {
console.error("Problem parsing feeds.section.topstories.options", e); // eslint-disable-line no-console
const optionJSON = props.Prefs.values["feeds.section.topstories.options"];
if (optionJSON) {
try {
this.topStoriesOptions = JSON.parse(optionJSON);
} catch (e) {
console.error("Problem parsing feeds.section.topstories.options", e); // eslint-disable-line no-console
}
}
}
componentDidMount() {
@ -2018,7 +1983,7 @@ module.exports.PreferencesPane = PreferencesPane;
module.exports.PreferencesInput = PreferencesInput;
/***/ }),
/* 19 */
/* 18 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -2117,7 +2082,7 @@ module.exports = connect()(injectIntl(Search));
module.exports._unconnected = Search;
/***/ }),
/* 20 */
/* 19 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -2133,12 +2098,32 @@ const connect = _require.connect;
var _require2 = __webpack_require__(2);
const FormattedMessage = _require2.FormattedMessage;
const injectIntl = _require2.injectIntl,
FormattedMessage = _require2.FormattedMessage;
const Card = __webpack_require__(13);
const Topics = __webpack_require__(22);
const Card = __webpack_require__(12);
const Topics = __webpack_require__(21);
class Section extends React.Component {
constructor(props) {
super(props);
this.onInfoEnter = this.onInfoEnter.bind(this);
this.onInfoLeave = this.onInfoLeave.bind(this);
this.state = { infoActive: false };
}
onInfoEnter() {
this.setState({ infoActive: true });
}
onInfoLeave(event) {
// If we have a related target, check to see if it is within the current
// target (section-info-option) to keep infoActive true. False otherwise.
this.setState({
infoActive: event && event.relatedTarget && event.relatedTarget.compareDocumentPosition(event.currentTarget) & Node.DOCUMENT_POSITION_CONTAINS
});
}
render() {
var _props = this.props;
const id = _props.id,
@ -2150,10 +2135,22 @@ class Section extends React.Component {
emptyState = _props.emptyState,
dispatch = _props.dispatch,
maxCards = _props.maxCards,
contextMenuOptions = _props.contextMenuOptions;
contextMenuOptions = _props.contextMenuOptions,
intl = _props.intl;
const initialized = rows && rows.length > 0;
const shouldShowTopics = id === "TopStories" && this.props.topics && this.props.read_more_endpoint;
const infoOptionIconA11yAttrs = {
"aria-haspopup": "true",
"aria-controls": "info-option",
"aria-expanded": this.state.infoActive ? "true" : "false",
"role": "note",
"tabIndex": 0
};
const sectionInfoTitle = intl.formatMessage({ id: "section_info_option" });
// <Section> <-- React component
// <section> <-- HTML5 element
return React.createElement(
@ -2170,19 +2167,19 @@ class Section extends React.Component {
),
infoOption && React.createElement(
"span",
{ className: "section-info-option" },
React.createElement(
"span",
{ className: "sr-only" },
React.createElement(FormattedMessage, { id: "section_info_option" })
),
React.createElement("img", { className: "info-option-icon" }),
{ className: "section-info-option",
onBlur: this.onInfoLeave,
onFocus: this.onInfoEnter,
onMouseOut: this.onInfoLeave,
onMouseOver: this.onInfoEnter },
React.createElement("img", _extends({ className: "info-option-icon", title: sectionInfoTitle
}, infoOptionIconA11yAttrs)),
React.createElement(
"div",
{ className: "info-option" },
infoOption.header && React.createElement(
"div",
{ className: "info-option-header" },
{ className: "info-option-header", role: "heading" },
React.createElement(FormattedMessage, infoOption.header)
),
infoOption.body && React.createElement(
@ -2222,23 +2219,26 @@ class Section extends React.Component {
}
}
const SectionIntl = injectIntl(Section);
class Sections extends React.Component {
render() {
const sections = this.props.Sections;
return React.createElement(
"div",
{ className: "sections-list" },
sections.map(section => React.createElement(Section, _extends({ key: section.id }, section, { dispatch: this.props.dispatch })))
sections.map(section => React.createElement(SectionIntl, _extends({ key: section.id }, section, { dispatch: this.props.dispatch })))
);
}
}
module.exports = connect(state => ({ Sections: state.Sections }))(Sections);
module.exports._unconnected = Sections;
module.exports.Section = Section;
module.exports.SectionIntl = SectionIntl;
module.exports._unconnectedSection = Section;
/***/ }),
/* 21 */
/* 20 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -2254,15 +2254,14 @@ var _require2 = __webpack_require__(2);
const FormattedMessage = _require2.FormattedMessage;
const shortURL = __webpack_require__(4);
const LinkMenu = __webpack_require__(5);
const LinkMenu = __webpack_require__(4);
var _require3 = __webpack_require__(1);
const ac = _require3.actionCreators,
at = _require3.actionTypes;
var _require4 = __webpack_require__(6);
var _require4 = __webpack_require__(5);
const perfSvc = _require4.perfService;
@ -2304,10 +2303,22 @@ class TopSite extends React.Component {
dispatch = _props.dispatch;
const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === index;
const title = link.pinTitle || shortURL(link);
const screenshotClassName = `screenshot${link.screenshot ? " active" : ""}`;
const title = link.pinTitle || link.hostname;
const topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`;
const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
const tippyTopIcon = link.tippyTopIcon;
let imageClassName;
let imageStyle;
if (tippyTopIcon) {
imageClassName = "tippy-top-icon";
imageStyle = {
backgroundColor: link.backgroundColor,
backgroundImage: `url(${tippyTopIcon})`
};
} else {
imageClassName = `screenshot${link.screenshot ? " active" : ""}`;
imageStyle = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
}
return React.createElement(
"li",
{ className: topSiteOuterClassName, key: link.guid || link.url },
@ -2322,7 +2333,7 @@ class TopSite extends React.Component {
{ className: "letter-fallback" },
title[0]
),
React.createElement("div", { className: screenshotClassName, style: style })
React.createElement("div", { className: imageClassName, style: imageStyle })
),
React.createElement(
"div",
@ -2473,7 +2484,7 @@ module.exports.TopSite = TopSite;
module.exports.TopSites = TopSites;
/***/ }),
/* 22 */
/* 21 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -2538,7 +2549,7 @@ module.exports._unconnected = Topics;
module.exports.Topic = Topic;
/***/ }),
/* 23 */
/* 22 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
@ -2549,13 +2560,12 @@ var _require = __webpack_require__(1);
const at = _require.actionTypes,
ac = _require.actionCreators;
const shortURL = __webpack_require__(4);
/**
* List of functions that return items that can be included as menu options in a
* LinkMenu. All functions take the site as the first parameter, and optionally
* the index of the site.
*/
module.exports = {
Separator: () => ({ type: "separator" }),
RemoveBookmark: site => ({
@ -2621,7 +2631,7 @@ module.exports = {
icon: "pin",
action: ac.SendToMain({
type: at.TOP_SITES_PIN,
data: { site: { url: site.url, title: shortURL(site) }, index }
data: { site: { url: site.url, title: site.hostname }, index }
}),
userEvent: "PIN"
}),
@ -2649,7 +2659,7 @@ module.exports.CheckBookmark = site => site.bookmarkGuid ? module.exports.Remove
module.exports.CheckPinTopSite = (site, index) => site.isPinned ? module.exports.UnpinTopSite(site) : module.exports.PinTopSite(site, index);
/***/ }),
/* 24 */
/* 23 */
/***/ (function(module, exports) {
var g;
@ -2676,35 +2686,35 @@ module.exports = g;
/***/ }),
/* 25 */
/* 24 */
/***/ (function(module, exports) {
module.exports = Redux;
/***/ }),
/* 26 */
/* 25 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
const React = __webpack_require__(0);
const ReactDOM = __webpack_require__(12);
const Base = __webpack_require__(7);
const ReactDOM = __webpack_require__(11);
const Base = __webpack_require__(6);
var _require = __webpack_require__(3);
const Provider = _require.Provider;
const initStore = __webpack_require__(9);
const initStore = __webpack_require__(8);
var _require2 = __webpack_require__(11);
var _require2 = __webpack_require__(10);
const reducers = _require2.reducers;
const DetectUserSessionStart = __webpack_require__(8);
const DetectUserSessionStart = __webpack_require__(7);
var _require3 = __webpack_require__(10);
var _require3 = __webpack_require__(9);
const addSnippetsSubscriber = _require3.addSnippetsSubscriber;

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

@ -297,6 +297,17 @@ main {
opacity: 0; }
.top-sites-list .top-site-outer .screenshot.active {
opacity: 1; }
.top-sites-list .top-site-outer .tippy-top-icon {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
border-radius: 6px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
background-position: center center;
background-size: 80px;
background-repeat: no-repeat; }
.top-sites-list .top-site-outer .title {
height: 30px;
line-height: 30px;
@ -332,12 +343,12 @@ main {
height: 16px;
width: 16px;
display: inline-block; }
.sections-list .section-top-bar .section-info-option div {
.sections-list .section-top-bar .section-info-option .info-option {
visibility: hidden;
opacity: 0;
transition: visibility 0.2s, opacity 0.2s ease-out;
transition-delay: 0.5s; }
.sections-list .section-top-bar .section-info-option:hover div {
.sections-list .section-top-bar .info-option-icon[aria-expanded="true"] + .info-option {
visibility: visible;
opacity: 1;
transition: visibility 0.2s, opacity 0.2s ease-out; }
@ -421,7 +432,7 @@ main {
text-align: center; }
.topic {
font-size: 13px;
font-size: 12px;
color: #BFC0C7;
margin-top: 16px;
line-height: 1.6; }
@ -460,7 +471,8 @@ main {
margin-left: 5px;
background-image: url("assets/topic-show-more-12.svg");
background-repeat: no-repeat;
vertical-align: middle; }
vertical-align: middle;
background-position-y: 1px; }
.search-wrapper {
cursor: default;

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.5 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.6 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.4 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.7 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.3 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.0 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.7 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.0 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.8 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.9 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.4 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.5 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.9 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.0 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.7 KiB

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

@ -0,0 +1,114 @@
[
{
"title": "aliexpress",
"url": "https://www.aliexpress.com/",
"image_url": "aliexpress-com.png",
"background_color": "#f6050a",
"domain": "aliexpress.com"
},
{
"title": "allegro",
"url": "https://www.allegro.pl/",
"image_url": "allegro-pl.png",
"background_color": "#ff4900",
"domain": "allegro.pl"
},
{
"title": "amazon",
"urls": ["https://www.amazon.com/", "https://www.amazon.ca/", "https://www.amazon.de/", "https://www.amazon.co.uk/", "https://www.amazon.fr/"],
"image_url": "amazon-com.png",
"background_color": "#FFF",
"domain": "amazon.com"
},
{
"title": "avito",
"url": "https://www.avito.ru/",
"image_url": "avito-ru.png",
"background_color": "#FFFFFF",
"domain": "avito.ru"
},
{
"title": "bbc",
"urls": ["http://www.bbc.com/", "https://www.bbc.co.uk/"],
"image_url": "bbc-com.png",
"background_color": "#000000",
"domain": "bbc.com"
},
{
"title": "ebay",
"urls": ["https://www.ebay.com", "https://ebay.de", "https://www.ebay.co.uk/"],
"image_url": "ebay-com.png",
"background_color": "#ededed",
"domain": "ebay.com"
},
{
"title": "facebook",
"url": "https://www.facebook.com/",
"image_url": "facebook-com.png",
"background_color": "#3b5998",
"domain": "facebook.com"
},
{
"title": "leboncoin",
"url": "http://www.leboncoin.fr/",
"image_url": "leboncoin-fr.png",
"background_color": "#ff6000",
"domain": "leboncoin.fr"
},
{
"title": "ok",
"url": "https://www.ok.ru/",
"image_url": "ok-ru.png",
"background_color": "#fb7b00",
"domain": "ok.ru"
},
{
"title": "olx",
"url": "https://www.olx.pl/",
"image_url": "olx-pl.png",
"background_color": "#b7c200",
"domain": "olx.pl"
},
{
"title": "reddit",
"url": "https://www.reddit.com/",
"image_url": "reddit-com.png",
"background_color": "#cee3f8",
"domain": "reddit.com"
},
{
"title": "twitter",
"url": "https://twitter.com/",
"image_url": "twitter-com.png",
"background_color": "#049ff5",
"domain": "twitter.com"
},
{
"title": "vk",
"url": "https://vk.com/",
"image_url": "vk-com.png",
"background_color": "#4483be",
"domain": "vk.com"
},
{
"title": "youtube",
"url": "https://www.youtube.com/",
"image_url": "youtube-com.png",
"background_color": "#db4338",
"domain": "youtube.com"
},
{
"title": "wikipedia",
"url": "https://www.wikipedia.org/",
"image_url": "wikipedia-org.png",
"background_color": "#fff",
"domain": "wikipedia.org"
},
{
"title": "wykop",
"url": "https://www.wykop.pl/",
"image_url": "wykop-pl.png",
"background_color": "#157ead",
"domain": "wykop.pl"
}
]

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

@ -1756,6 +1756,7 @@
"header_stories": "برترین داستان‌ها",
"header_visit_again": "مشاهده دوباره",
"header_bookmarks": "نشانک‌های اخیر",
"header_recommended_by": "پیشنهاد شده توسط {provider}",
"header_bookmarks_placeholder": "هنوز هیچ نشانکی ندارید.",
"header_stories_from": "از",
"type_label_visited": "مشاهده شده",
@ -1764,6 +1765,7 @@
"type_label_recommended": "موضوعات داغ",
"type_label_open": "باز کردن",
"type_label_topic": "موضوع",
"type_label_now": "هم‌اکنون",
"menu_action_bookmark": "نشانک",
"menu_action_remove_bookmark": "حذف نشانک",
"menu_action_copy_address": "رونوشت از آدرس",
@ -1782,6 +1784,7 @@
"search_header": "جست‌وجو {search_engine_name}",
"search_web_placeholder": "جست‌وجوی وب",
"search_settings": "تغییر تنظیمات جست‌وجو",
"section_info_option": "اطلاعات",
"welcome_title": "به زبانه جدید خوش‌آمدید",
"welcome_body": "فایرفاکس از این فضا برای نمایش نشانک‌ها، مقالات، ویدئوها و صفحات مرتبطی که به‌تازگی مشاهده کرده‌اید استفاده می‌کند، تا شما به راحتی دوباره به آنها دسترسی داشته باشید.",
"welcome_label": "شناسایی گزینه‌های برجسته شما",
@ -1826,7 +1829,10 @@
"pocket_read_even_more": "مشاهده داستان‌های بیشتر",
"pocket_feedback_header": "بهترین‌های وب، گزینش شده توسط بیش از ۲۵ میلیون نفر.",
"pocket_feedback_body": "Pocket، بخشی از خانواده موزیلا، کمک خواهد کرد تا به محتوایی با کیفیت بالا مرتبط شوید که در غیر این صورت ممکن بود پیدا نکنید.",
"pocket_send_feedback": "ارسال بازخورد"
"pocket_send_feedback": "ارسال بازخورد",
"manual_migration_explanation": "فایرفاکس را با سایت‌های مورد علاقه و نشانک‌های خود در سایر مرورگرها امتحان کنید.",
"manual_migration_cancel_button": "نه ممنون",
"manual_migration_import_button": "هم‌اکنون وارد شوند"
},
"ff": {},
"fi": {
@ -3901,18 +3907,18 @@
"nn-NO": {
"newtab_page_title": "Ny fane",
"default_label_loading": "Lastar…",
"header_top_sites": "Mest vitja",
"header_top_sites": "Mest besøkte nettsider",
"header_stories": "Hovudsakene",
"header_visit_again": "Bes;kigjen",
"header_visit_again": "Besøk igjen",
"header_bookmarks": "Nylege bokmerke",
"header_recommended_by": "Tilrådd av {provider}",
"header_bookmarks_placeholder": "Du har ingen bokmerke enno.",
"header_stories_from": "frå",
"type_label_visited": "Vitja",
"type_label_visited": "Besøkt",
"type_label_bookmarked": "Bokmerkte",
"type_label_synced": "Synkronisert frå ei anna eining",
"type_label_recommended": "Trendar",
"type_label_open": "Opna",
"type_label_open": "Opne",
"type_label_topic": "Emne",
"type_label_now": "No",
"menu_action_bookmark": "Bokmerke",
@ -3920,7 +3926,7 @@
"menu_action_copy_address": "Kopier adresse",
"menu_action_email_link": "E-postlenke…",
"menu_action_open_new_window": "Opne i nytt vindauge",
"menu_action_open_private_window": "Opna i eit nytt privat vindauge",
"menu_action_open_private_window": "Opne i eit nytt privat vindauge",
"menu_action_dismiss": "Avslå",
"menu_action_delete": "Slett frå historikk",
"menu_action_pin": "Fest",
@ -3935,7 +3941,7 @@
"search_settings": "Endra søkjeinnstillingar",
"section_info_option": "Info",
"welcome_title": "Velkomen til ny fane",
"welcome_body": "Firefox vil bruka denne plassen til å visa deg dei mest relevante bokmerka, artiklane, videoane og sidene du nettopp har vitja, slik at du enkelt kan finna tilbake til dei.",
"welcome_body": "Firefox vil bruke denne plassen til å vise deg dei mest relevante bokmerka, artiklane, videoane og sidene du nettopp har vitja, slik at du enkelt kan finne tilbake til dei.",
"welcome_label": "Identifiserer høgdepunkta dine",
"time_label_less_than_minute": "<1 min.",
"time_label_minute": "{number} m",
@ -3966,8 +3972,8 @@
"edit_topsites_edit_button": "Rediger denne nettsida",
"edit_topsites_dismiss_button": "Avvis denne nettsida",
"edit_topsites_add_button": "Legg til",
"topsites_form_add_header": "Ny toppstad",
"topsites_form_edit_header": "Rediger mest vitja",
"topsites_form_add_header": "Ny Mest besøkt",
"topsites_form_edit_header": "Rediger Mest besøkt",
"topsites_form_title_placeholder": "Skriv inn ein tittel",
"topsites_form_url_placeholder": "Skriv eller lim inn ein URL",
"topsites_form_add_button": "Legg til",

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

@ -8,7 +8,7 @@ Cu.import("resource://gre/modules/Services.jsm");
// NB: Eagerly load modules that will be loaded/constructed/initialized in the
// common case to avoid the overhead of wrapping and detecting lazy loading.
const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {DefaultPrefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
const {LocalizationFeed} = Cu.import("resource://activity-stream/lib/LocalizationFeed.jsm", {});
const {ManualMigration} = Cu.import("resource://activity-stream/lib/ManualMigration.jsm", {});
@ -147,7 +147,7 @@ const FEEDS_DATA = [
name: "snippets",
factory: () => new SnippetsFeed(),
title: "Gets snippets data",
value: false
value: true
},
{
name: "systemtick",
@ -197,11 +197,12 @@ this.ActivityStream = class ActivityStream {
this._updateDynamicPrefs();
this._defaultPrefs.init();
// Hook up the store and let all feeds and pages initialize
this.store.init(this.feeds);
this.store.dispatch({
this.store.dispatch(ac.BroadcastToContent({
type: at.INIT,
data: {version: this.options.version}
});
}));
this.initialized = true;
}

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

@ -14,8 +14,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
// Url to fetch snippets, in the urlFormatter service format.
const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
const TELEMETRY_PREF = "datareporting.healthreport.uploadEnabled";
const ONBOARDING_FINISHED_PREF = "browser.onboarding.notification.finished";
const FXA_USERNAME_PREF = "services.sync.username";
// Should be bumped up if the snippets content format changes.
const STARTPAGE_VERSION = 5;
@ -48,22 +48,29 @@ this.SnippetsFeed = class SnippetsFeed {
version: STARTPAGE_VERSION,
profileCreatedWeeksAgo: profileInfo.createdWeeksAgo,
profileResetWeeksAgo: profileInfo.resetWeeksAgo,
telemetryEnabled: Services.prefs.getBoolPref(TELEMETRY_PREF),
onboardingFinished: Services.prefs.getBoolPref(ONBOARDING_FINISHED_PREF)
telemetryEnabled: Services.telemetry.canRecordBase,
onboardingFinished: Services.prefs.getBoolPref(ONBOARDING_FINISHED_PREF),
fxaccount: Services.prefs.prefHasUserValue(FXA_USERNAME_PREF)
};
this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_DATA, data}));
}
_refreshCanRecordBase() {
// TODO: There is currently no way to listen for changes to this value, so
// we are just refreshing it on every new tab instead. A bug is filed
// here to fix this: https://bugzilla.mozilla.org/show_bug.cgi?id=1386318
this.store.dispatch({type: at.SNIPPETS_DATA, data: {telemetryEnabled: Services.telemetry.canRecordBase}});
}
async init() {
await this._refresh();
Services.prefs.addObserver(ONBOARDING_FINISHED_PREF, this._refresh);
Services.prefs.addObserver(SNIPPETS_URL_PREF, this._refresh);
Services.prefs.addObserver(TELEMETRY_PREF, this._refresh);
Services.prefs.addObserver(FXA_USERNAME_PREF, this._refresh);
}
uninit() {
Services.prefs.removeObserver(ONBOARDING_FINISHED_PREF, this._refresh);
Services.prefs.removeObserver(SNIPPETS_URL_PREF, this._refresh);
Services.prefs.removeObserver(TELEMETRY_PREF, this._refresh);
Services.prefs.removeObserver(FXA_USERNAME_PREF, this._refresh);
this.store.dispatch({type: at.SNIPPETS_RESET});
}
onAction(action) {
@ -74,6 +81,9 @@ this.SnippetsFeed = class SnippetsFeed {
case at.FEED_INIT:
if (action.data === "feeds.snippets") { this.init(); }
break;
case at.NEW_TAB_INIT:
this._refreshCanRecordBase();
break;
}
}
};

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

@ -6,6 +6,7 @@ const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.importGlobalProperties(["fetch"]);
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
@ -19,8 +20,6 @@ const ENDPOINT_PREF = `${PREF_BRANCH}telemetry.ping.endpoint`;
const TELEMETRY_PREF = `${PREF_BRANCH}telemetry`;
const LOGGING_PREF = `${PREF_BRANCH}telemetry.log`;
const FHR_UPLOAD_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
/**
* Observe various notifications and send them to a telemetry endpoint.
*
@ -44,10 +43,6 @@ function TelemetrySender(args) {
this._onTelemetryPrefChange = this._onTelemetryPrefChange.bind(this);
this._prefs.observe(TELEMETRY_PREF, this._onTelemetryPrefChange);
this._fhrEnabled = this._prefs.get(FHR_UPLOAD_ENABLED_PREF);
this._onFhrPrefChange = this._onFhrPrefChange.bind(this);
this._prefs.observe(FHR_UPLOAD_ENABLED_PREF, this._onFhrPrefChange);
this.logging = this._prefs.get(LOGGING_PREF);
this._onLoggingPrefChange = this._onLoggingPrefChange.bind(this);
this._prefs.observe(LOGGING_PREF, this._onLoggingPrefChange);
@ -57,7 +52,9 @@ function TelemetrySender(args) {
TelemetrySender.prototype = {
get enabled() {
return this._enabled && this._fhrEnabled;
// Note: Services.telemetry.canRecordBase is the general indicator for
// opt-out Firefox Telemetry
return this._enabled && Services.telemetry.canRecordBase;
},
_onLoggingPrefChange(prefVal) {
@ -68,10 +65,6 @@ TelemetrySender.prototype = {
this._enabled = prefVal;
},
_onFhrPrefChange(prefVal) {
this._fhrEnabled = prefVal;
},
sendPing(data) {
if (this.logging) {
// performance related pings cause a lot of logging, so we mute them
@ -95,7 +88,6 @@ TelemetrySender.prototype = {
try {
this._prefs.ignore(TELEMETRY_PREF, this._onTelemetryPrefChange);
this._prefs.ignore(LOGGING_PREF, this._onLoggingPrefChange);
this._prefs.ignore(FHR_UPLOAD_ENABLED_PREF, this._onFhrPrefChange);
} catch (e) {
Cu.reportError(e);
}
@ -105,7 +97,6 @@ TelemetrySender.prototype = {
this.TelemetrySender = TelemetrySender;
this.TelemetrySenderConstants = {
ENDPOINT_PREF,
FHR_UPLOAD_ENABLED_PREF,
TELEMETRY_PREF,
LOGGING_PREF
};

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

@ -0,0 +1,61 @@
/* 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/. */
const {utils: Cu} = Components;
Cu.importGlobalProperties(["fetch", "URL"]);
const TIPPYTOP_JSON_PATH = "resource://activity-stream/data/content/tippytop/top_sites.json";
const TIPPYTOP_URL_PREFIX = "resource://activity-stream/data/content/tippytop/images/";
function getDomain(url) {
let domain = new URL(url).hostname;
if (domain && domain.startsWith("www.")) {
domain = domain.slice(4);
}
return domain;
}
function getPath(url) {
return new URL(url).pathname;
}
this.TippyTopProvider = class TippyTopProvider {
constructor() {
this._sitesByDomain = new Map();
}
async init() {
// Load the Tippy Top sites from the json manifest.
try {
for (const site of await (await fetch(TIPPYTOP_JSON_PATH)).json()) {
// The tippy top manifest can have a url property (string) or a
// urls property (array of strings)
for (const url of site.url ? [site.url] : site.urls || []) {
this._sitesByDomain.set(getDomain(url), site);
}
}
} catch (error) {
Cu.reportError("Failed to load tippy top manifest.");
}
}
processSite(site) {
// Skip URLs with a path that isn't the root path /
let path;
try {
path = getPath(site.url);
} catch (e) {}
if (path !== "/") {
return site;
}
const tippyTop = this._sitesByDomain.get(getDomain(site.url));
if (tippyTop) {
site.tippyTopIcon = TIPPYTOP_URL_PREFIX + tippyTop.image_url;
site.backgroundColor = tippyTop.background_color;
}
return site;
}
};
this.EXPORTED_SYMBOLS = ["TippyTopProvider"];

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

@ -7,7 +7,10 @@ const {utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {TippyTopProvider} = Cu.import("resource://activity-stream/lib/TippyTopProvider.jsm", {});
const {insertPinned} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
const {shortURL} = Cu.import("resource://activity-stream/common/ShortURL.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
@ -22,6 +25,12 @@ const DEFAULT_TOP_SITES = [];
this.TopSitesFeed = class TopSitesFeed {
constructor() {
this.lastUpdated = 0;
this._tippyTopProvider = new TippyTopProvider();
this._tippyTopProvider.init();
this.dedupe = new Dedupe(this._dedupeKey);
}
_dedupeKey(site) {
return site && site.hostname;
}
refreshDefaults(sites) {
// Clear out the array of any previous defaults
@ -54,7 +63,17 @@ this.TopSitesFeed = class TopSitesFeed {
frecent = frecent.filter(link => link && link.type !== "affiliate");
}
return insertPinned([...frecent, ...DEFAULT_TOP_SITES], pinned).slice(0, TOP_SITES_SHOWMORE_LENGTH);
// Group together websites that require deduping.
let topsitesGroup = [];
for (const group of [pinned, frecent, DEFAULT_TOP_SITES]) {
topsitesGroup.push(group.filter(site => site).map(site => Object.assign({}, site, {hostname: shortURL(site)})));
}
const dedupedGroups = this.dedupe.group(topsitesGroup);
// Insert original pinned websites in the result of the dedupe operation.
pinned = insertPinned([...dedupedGroups[1], ...dedupedGroups[2]], pinned);
return pinned.slice(0, TOP_SITES_SHOWMORE_LENGTH);
}
async refresh(target = null) {
const links = await this.getLinksWithDefaults();
@ -67,9 +86,15 @@ this.TopSitesFeed = class TopSitesFeed {
}
}
// Now, get a screenshot for every item
// Now, get a tippy top icon or screenshot for every item
for (let link of links) {
if (!link) { continue; }
// Check for tippy top icon.
link = this._tippyTopProvider.processSite(link);
if (link.tippyTopIcon) { continue; }
// If no tippy top, then we get a screenshot.
if (currentScreenshots[link.url]) {
link.screenshot = currentScreenshots[link.url];
} else {
@ -110,15 +135,9 @@ this.TopSitesFeed = class TopSitesFeed {
}));
}
onAction(action) {
let realRows;
switch (action.type) {
case at.NEW_TAB_LOAD:
// Only check against real rows returned from history, not default ones.
realRows = this.store.getState().TopSites.rows.filter(row => !row.isDefault);
if (
// When a new tab is opened, if we don't have enough top sites yet, refresh the data.
(realRows.length < TOP_SITES_SHOWMORE_LENGTH) ||
// When a new tab is opened, if the last time we refreshed the data
// is greater than 15 minutes, refresh the data.
(Date.now() - this.lastUpdated >= UPDATE_TIME)

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

@ -11,6 +11,7 @@ Cu.importGlobalProperties(["fetch"]);
const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
const {shortURL} = Cu.import("resource://activity-stream/common/ShortURL.jsm", {});
const STORIES_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
const TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours
@ -85,6 +86,7 @@ this.TopStoriesFeed = class TopStoriesFeed {
.filter(s => !NewTabUtils.blockedLinks.isBlocked({"url": s.dedupe_url}))
.map(s => ({
"guid": s.id,
"hostname": shortURL(Object.assign({}, s, {url: s.dedupe_url})),
"type": (Date.now() - (s.published_timestamp * 1000)) <= STORIES_NOW_THRESHOLD ? "now" : "trending",
"title": s.title,
"description": s.excerpt,

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

@ -0,0 +1,42 @@
const {Dedupe} = require("common/Dedupe.jsm");
describe("Dedupe", () => {
let instance;
beforeEach(() => {
instance = new Dedupe();
});
describe("group", () => {
it("should remove duplicates inside the groups", () => {
const beforeItems = [[1, 1, 1], [2, 2, 2], [3, 3, 3]];
const afterItems = [[1], [2], [3]];
assert.deepEqual(instance.group(beforeItems), afterItems);
});
it("should remove duplicates between groups, favouring earlier groups", () => {
const beforeItems = [[1, 2, 3], [2, 3, 4], [3, 4, 5]];
const afterItems = [[1, 2, 3], [4], [5]];
assert.deepEqual(instance.group(beforeItems), afterItems);
});
it("should remove duplicates from groups of objects", () => {
instance = new Dedupe(item => item.id);
const beforeItems = [[{id: 1}, {id: 1}, {id: 2}], [{id: 1}, {id: 3}, {id: 2}], [{id: 1}, {id: 2}, {id: 5}]];
const afterItems = [[{id: 1}, {id: 2}], [{id: 3}], [{id: 5}]];
assert.deepEqual(instance.group(beforeItems), afterItems);
});
it("should take a custom comparison function", () => {
function compare(previous, current) {
return current.amount > previous.amount;
}
instance = new Dedupe(item => item.id, compare);
const beforeItems = [
[{id: 1, amount: 50}, {id: 1, amount: 100}],
[{id: 1, amount: 200}, {id: 2, amount: 0}, {id: 2, amount: 100}]
];
const afterItems = [
[{id: 1, amount: 100}],
[{id: 2, amount: 100}]
];
assert.deepEqual(instance.group(beforeItems), afterItems);
});
});
});

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

@ -9,14 +9,18 @@ describe("Reducers", () => {
const nextState = App(undefined, {type: "FOO"});
assert.equal(nextState, INITIAL_STATE.App);
});
it("should not set initialized to true on INIT", () => {
it("should set initialized to true on INIT", () => {
const nextState = App(undefined, {type: "INIT"});
assert.propertyVal(nextState, "initialized", true);
});
it("should set initialized, version, and locale on INIT", () => {
const action = {type: "INIT", data: {version: "1.2.3"}};
const nextState = App(undefined, action);
assert.propertyVal(nextState, "version", "1.2.3");
assert.propertyVal(nextState, "locale", INITIAL_STATE.App.locale);
});
it("should not update state for empty action.data on LOCALE_UPDATED", () => {
const nextState = App(undefined, {type: at.LOCALE_UPDATED});

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

@ -0,0 +1,46 @@
const {shortURL} = require("common/ShortURL.jsm");
describe("shortURL", () => {
it("should return a blank string if url and hostname is falsey", () => {
assert.equal(shortURL({url: ""}), "");
assert.equal(shortURL({hostname: null}), "");
});
it("should remove the eTLD, if provided", () => {
assert.equal(shortURL({hostname: "com.blah.com", eTLD: "com"}), "com.blah");
});
it("should use the hostname, if provided", () => {
assert.equal(shortURL({hostname: "foo.com", url: "http://bar.com", eTLD: "com"}), "foo");
});
it("should get the hostname from .url if necessary", () => {
assert.equal(shortURL({url: "http://bar.com", eTLD: "com"}), "bar");
});
it("should not strip out www if not first subdomain", () => {
assert.equal(shortURL({hostname: "foo.www.com", eTLD: "com"}), "foo.www");
});
it("should convert to lowercase", () => {
assert.equal(shortURL({url: "HTTP://FOO.COM", eTLD: "com"}), "foo");
});
it("should return hostname for localhost", () => {
assert.equal(shortURL({url: "http://localhost:8000/", eTLD: "localhost"}), "localhost");
});
it("should fallback to link title if it exists", () => {
const link = {
url: "file:///Users/voprea/Work/activity-stream/logs/coverage/system-addon/report-html/index.html",
title: "Code coverage report"
};
assert.equal(shortURL(link), link.title);
});
it("should return the url if no hostname or title is provided", () => {
const url = "file://foo/bar.txt";
assert.equal(shortURL({url, eTLD: "foo"}), url);
});
});

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

@ -1,4 +1,5 @@
const injector = require("inject!lib/ActivityStream.jsm");
const {CONTENT_MESSAGE_TYPE} = require("common/Actions.jsm");
const REASON_ADDON_UNINSTALL = 6;
@ -63,6 +64,14 @@ describe("ActivityStream", () => {
const action = as.store.dispatch.firstCall.args[0];
assert.propertyVal(action.data, "version", "1.2.3");
});
it("should emit an INIT event to content", () => {
sandbox.stub(as.store, "dispatch");
as.init();
const action = as.store.dispatch.firstCall.args[0];
assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
});
});
describe("#uninit", () => {
beforeEach(() => {

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

@ -10,16 +10,21 @@ describe("ActivityStreamMessageChannel", () => {
let dispatch;
let mm;
beforeEach(() => {
function RP(url) {
function RP(url, isFromAboutNewTab = false) {
this.url = url;
this.messagePorts = [];
this.addMessageListener = globals.sandbox.spy();
this.removeMessageListener = globals.sandbox.spy();
this.sendAsyncMessage = globals.sandbox.spy();
this.destroy = globals.sandbox.spy();
this.isFromAboutNewTab = isFromAboutNewTab;
}
globals = new GlobalOverrider();
const override = globals.sandbox.stub();
override.withArgs(true).returns(new RP("about:newtab", true));
override.withArgs(false).returns(null);
globals.set("AboutNewTab", {
override: globals.sandbox.spy(),
override,
reset: globals.sandbox.spy()
});
globals.set("RemotePages", RP);
@ -64,6 +69,10 @@ describe("ActivityStreamMessageChannel", () => {
mm.createChannel();
assert.calledOnce(global.AboutNewTab.override);
});
it("should use the channel passed by AboutNewTab on override", () => {
mm.createChannel();
assert.ok(mm.channel.isFromAboutNewTab);
});
it("should not override AboutNewTab if the pageURL is not about:newtab", () => {
mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
mm.createChannel();
@ -76,17 +85,14 @@ describe("ActivityStreamMessageChannel", () => {
mm.createChannel();
channel = mm.channel;
});
it("should call channel.destroy()", () => {
mm.destroyChannel();
assert.calledOnce(channel.destroy);
});
it("should set .channel to null", () => {
mm.destroyChannel();
assert.isNull(mm.channel);
});
it("should reset AboutNewTab", () => {
it("should reset AboutNewTab, and pass back its channel", () => {
mm.destroyChannel();
assert.calledOnce(global.AboutNewTab.reset);
assert.calledWith(global.AboutNewTab.reset, channel);
});
it("should not reset AboutNewTab if the pageURL is not about:newtab", () => {
mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
@ -94,6 +100,13 @@ describe("ActivityStreamMessageChannel", () => {
mm.destroyChannel();
assert.notCalled(global.AboutNewTab.reset);
});
it("should call channel.destroy() if pageURL is not about:newtab", () => {
mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
mm.createChannel();
channel = mm.channel;
mm.destroyChannel();
assert.calledOnce(channel.destroy);
});
});
});
describe("Message handling", () => {

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

@ -1,6 +1,8 @@
const {SnippetsFeed} = require("lib/SnippetsFeed.jsm");
const {actionTypes: at} = require("common/Actions.jsm");
const {GlobalOverrider} = require("test/unit/utils");
const {createStore, combineReducers} = require("redux");
const {reducers} = require("common/Reducers.jsm");
const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
@ -8,6 +10,7 @@ let overrider = new GlobalOverrider();
describe("SnippetsFeed", () => {
let sandbox;
let store;
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers();
@ -20,6 +23,8 @@ describe("SnippetsFeed", () => {
}
});
sandbox = sinon.sandbox.create();
store = createStore(combineReducers(reducers));
sinon.spy(store, "dispatch");
});
afterEach(() => {
clock.restore();
@ -30,34 +35,43 @@ describe("SnippetsFeed", () => {
const url = "foo.com/%STARTPAGE_VERSION%";
sandbox.stub(global.Services.prefs, "getStringPref").returns(url);
sandbox.stub(global.Services.prefs, "getBoolPref")
.withArgs("datareporting.healthreport.uploadEnabled")
.returns(true)
.withArgs("browser.onboarding.notification.finished")
.returns(false);
sandbox.stub(global.Services.prefs, "prefHasUserValue")
.withArgs("services.sync.username")
.returns(true);
sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
const feed = new SnippetsFeed();
feed.store = {dispatch: sandbox.stub()};
feed.store = store;
clock.tick(WEEK_IN_MS * 2);
await feed.init();
assert.calledOnce(feed.store.dispatch);
const state = store.getState().Snippets;
const action = feed.store.dispatch.firstCall.args[0];
assert.propertyVal(action, "type", at.SNIPPETS_DATA);
assert.isObject(action.data);
assert.propertyVal(action.data, "snippetsURL", "foo.com/5");
assert.propertyVal(action.data, "version", 5);
assert.propertyVal(action.data, "profileCreatedWeeksAgo", 2);
assert.propertyVal(action.data, "profileResetWeeksAgo", 1);
assert.propertyVal(action.data, "telemetryEnabled", true);
assert.propertyVal(action.data, "onboardingFinished", false);
assert.propertyVal(state, "snippetsURL", "foo.com/5");
assert.propertyVal(state, "version", 5);
assert.propertyVal(state, "profileCreatedWeeksAgo", 2);
assert.propertyVal(state, "profileResetWeeksAgo", 1);
assert.propertyVal(state, "telemetryEnabled", false);
assert.propertyVal(state, "onboardingFinished", false);
assert.propertyVal(state, "fxaccount", true);
});
it("should update telemetryEnabled on each new tab", () => {
sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
const feed = new SnippetsFeed();
feed.store = store;
feed.onAction({type: at.NEW_TAB_INIT});
const state = store.getState().Snippets;
assert.propertyVal(state, "telemetryEnabled", false);
});
it("should call .init on an INIT aciton", () => {
const feed = new SnippetsFeed();
sandbox.stub(feed, "init");
feed.store = {dispatch: sandbox.stub()};
feed.store = store;
feed.onAction({type: at.INIT});
assert.calledOnce(feed.init);
@ -65,7 +79,7 @@ describe("SnippetsFeed", () => {
it("should call .init when a FEED_INIT happens for feeds.snippets", () => {
const feed = new SnippetsFeed();
sandbox.stub(feed, "init");
feed.store = {dispatch: sandbox.stub()};
feed.store = store;
feed.onAction({type: at.FEED_INIT, data: "feeds.snippets"});
@ -73,7 +87,7 @@ describe("SnippetsFeed", () => {
});
it("should dispatch a SNIPPETS_RESET on uninit", () => {
const feed = new SnippetsFeed();
feed.store = {dispatch: sandbox.stub()};
feed.store = store;
feed.uninit();

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

@ -3,7 +3,7 @@
const {GlobalOverrider, FakePrefs} = require("test/unit/utils");
const {TelemetrySender, TelemetrySenderConstants} = require("lib/TelemetrySender.jsm");
const {ENDPOINT_PREF, FHR_UPLOAD_ENABLED_PREF, TELEMETRY_PREF, LOGGING_PREF} =
const {ENDPOINT_PREF, TELEMETRY_PREF, LOGGING_PREF} =
TelemetrySenderConstants;
/**
@ -52,15 +52,15 @@ describe("TelemetrySender", () => {
describe("#enabled", () => {
let testParams = [
{enabledPref: true, fhrPref: true, result: true},
{enabledPref: false, fhrPref: true, result: false},
{enabledPref: true, fhrPref: false, result: false},
{enabledPref: false, fhrPref: false, result: false}
{enabledPref: true, canRecordBase: true, result: true},
{enabledPref: false, canRecordBase: true, result: false},
{enabledPref: true, canRecordBase: false, result: false},
{enabledPref: false, canRecordBase: false, result: false}
];
function testEnabled(p) {
FakePrefs.prototype.prefs[TELEMETRY_PREF] = p.enabledPref;
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = p.fhrPref;
sandbox.stub(global.Services.telemetry, "canRecordBase").value(p.canRecordBase);
tSender = new TelemetrySender(tsArgs);
@ -68,7 +68,7 @@ describe("TelemetrySender", () => {
}
for (let p of testParams) {
it(`should return ${p.result} if the fhrPref is ${p.fhrPref} and telemetry.enabled is ${p.enabledPref}`, () => {
it(`should return ${p.result} if the Services.telemetry.canRecordBase is ${p.canRecordBase} and telemetry.enabled is ${p.enabledPref}`, () => {
testEnabled(p);
});
}
@ -77,7 +77,7 @@ describe("TelemetrySender", () => {
beforeEach(() => {
FakePrefs.prototype.prefs = {};
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
tSender = new TelemetrySender(tsArgs);
assert.propertyVal(tSender, "enabled", true);
});
@ -92,7 +92,7 @@ describe("TelemetrySender", () => {
describe("telemetry.enabled pref changes from false to true", () => {
beforeEach(() => {
FakePrefs.prototype.prefs = {};
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;
tSender = new TelemetrySender(tsArgs);
@ -106,26 +106,26 @@ describe("TelemetrySender", () => {
});
});
describe("FHR enabled pref changes from true to false", () => {
describe("canRecordBase changes from true to false", () => {
beforeEach(() => {
FakePrefs.prototype.prefs = {};
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
tSender = new TelemetrySender(tsArgs);
assert.propertyVal(tSender, "enabled", true);
});
it("should set the enabled property to false", () => {
fakePrefs.set(FHR_UPLOAD_ENABLED_PREF, false);
sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
assert.propertyVal(tSender, "enabled", false);
});
});
describe("FHR enabled pref changes from false to true", () => {
describe("canRecordBase changes from false to true", () => {
beforeEach(() => {
FakePrefs.prototype.prefs = {};
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = false;
sandbox.stub(global.Services.telemetry, "canRecordBase").value(false);
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
tSender = new TelemetrySender(tsArgs);
@ -133,7 +133,7 @@ describe("TelemetrySender", () => {
});
it("should set the enabled property to true", () => {
fakePrefs.set(FHR_UPLOAD_ENABLED_PREF, true);
sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
assert.propertyVal(tSender, "enabled", true);
});
@ -143,7 +143,7 @@ describe("TelemetrySender", () => {
describe("#sendPing()", () => {
beforeEach(() => {
FakePrefs.prototype.prefs = {};
FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;
sandbox.stub(global.Services.telemetry, "canRecordBase").value(true);
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
FakePrefs.prototype.prefs[ENDPOINT_PREF] = fakeEndpointUrl;
tSender = new TelemetrySender(tsArgs);
@ -208,15 +208,6 @@ describe("TelemetrySender", () => {
assert.notProperty(fakePrefs.observers, TELEMETRY_PREF);
});
it("should remove the fhrpref listener", () => {
tSender = new TelemetrySender(tsArgs);
assert.property(fakePrefs.observers, FHR_UPLOAD_ENABLED_PREF);
tSender.uninit();
assert.notProperty(fakePrefs.observers, FHR_UPLOAD_ENABLED_PREF);
});
it("should remove the telemetry log listener", () => {
tSender = new TelemetrySender(tsArgs);
assert.property(fakePrefs.observers, LOGGING_PREF);

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

@ -0,0 +1,71 @@
"use strict";
const {TippyTopProvider} = require("lib/TippyTopProvider.jsm");
const {GlobalOverrider} = require("test/unit/utils");
describe("TippyTopProvider", () => {
let instance;
let globals;
beforeEach(async () => {
globals = new GlobalOverrider();
let fetchStub = globals.sandbox.stub();
globals.set("fetch", fetchStub);
fetchStub.resolves({
ok: true,
status: 200,
json: () => Promise.resolve([{
"title": "facebook",
"url": "https://www.facebook.com/",
"image_url": "facebook-com.png",
"background_color": "#3b5998",
"domain": "facebook.com"
}, {
"title": "gmail",
"urls": ["https://www.gmail.com/", "https://mail.google.com"],
"image_url": "gmail-com.png",
"background_color": "#000000",
"domain": "gmail.com"
}])
});
instance = new TippyTopProvider();
await instance.init();
});
it("should provide an icon for facebook.com", () => {
const site = instance.processSite({url: "https://facebook.com"});
assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/facebook-com.png");
assert.equal(site.backgroundColor, "#3b5998");
});
it("should provide an icon for www.facebook.com", () => {
const site = instance.processSite({url: "https://www.facebook.com"});
assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/facebook-com.png");
assert.equal(site.backgroundColor, "#3b5998");
});
it("should not provide an icon for facebook.com/foobar", () => {
const site = instance.processSite({url: "https://facebook.com/foobar"});
assert.isUndefined(site.tippyTopIcon);
assert.isUndefined(site.backgroundColor);
});
it("should provide an icon for gmail.com", () => {
const site = instance.processSite({url: "https://gmail.com"});
assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/gmail-com.png");
assert.equal(site.backgroundColor, "#000000");
});
it("should provide an icon for mail.google.com", () => {
const site = instance.processSite({url: "https://mail.google.com"});
assert.equal(site.tippyTopIcon, "resource://activity-stream/data/content/tippytop/images/gmail-com.png");
assert.equal(site.backgroundColor, "#000000");
});
it("should handle garbage URLs gracefully", () => {
const site = instance.processSite({url: "garbagejlfkdsa"});
assert.isUndefined(site.tippyTopIcon);
assert.isUndefined(site.backgroundColor);
});
it("should handle error when fetching and parsing manifest", async () => {
globals = new GlobalOverrider();
let fetchStub = globals.sandbox.stub();
globals.set("fetch", fetchStub);
fetchStub.rejects("whaaaa");
instance = new TippyTopProvider();
await instance.init();
instance.processSite("https://facebook.com");
});
});

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

@ -5,9 +5,15 @@ const {FakePrefs, GlobalOverrider} = require("test/unit/utils");
const action = {meta: {fromTarget: {}}};
const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
const {insertPinned} = require("common/Reducers.jsm");
const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `site${i}.com`}));
const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `http://www.site${i}.com`}));
const FAKE_SCREENSHOT = "data123";
function FakeTippyTopProvider() {}
FakeTippyTopProvider.prototype = {
init() {},
processSite(site) { return site; }
};
describe("Top Sites Feed", () => {
let TopSitesFeed;
let DEFAULT_TOP_SITES;
@ -18,6 +24,7 @@ describe("Top Sites Feed", () => {
let clock;
let fakeNewTabUtils;
let fakeScreenshot;
let shortURLStub;
beforeEach(() => {
globals = new GlobalOverrider();
@ -32,15 +39,21 @@ describe("Top Sites Feed", () => {
}
};
fakeScreenshot = {getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))};
shortURLStub = sinon.stub().callsFake(site => site.url);
const fakeDedupe = function() {};
globals.set("NewTabUtils", fakeNewTabUtils);
FakePrefs.prototype.prefs["default.sites"] = "https://foo.com/";
({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
"common/Dedupe.jsm": {Dedupe: fakeDedupe},
"common/Reducers.jsm": {insertPinned},
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot}
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot},
"lib/TippyTopProvider.jsm": {TippyTopProvider: FakeTippyTopProvider},
"common/ShortURL.jsm": {shortURL: shortURLStub}
}));
feed = new TopSitesFeed();
feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
feed.dedupe.group = sites => sites;
links = FAKE_LINKS;
clock = sinon.useFakeTimers();
});
@ -84,19 +97,42 @@ describe("Top Sites Feed", () => {
it("should get the links from NewTabUtils", async () => {
const result = await feed.getLinksWithDefaults();
assert.deepEqual(result, links);
const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
assert.deepEqual(result, reference);
assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
});
it("should call dedupe on the links", async () => {
const stub = sinon.stub(feed.dedupe, "group", id => id);
await feed.getLinksWithDefaults();
assert.calledOnce(stub);
});
it("should dedupe the links by hostname", async () => {
const site = {url: "foo", hostname: "bar"};
const result = feed._dedupeKey(site);
assert.equal(result, site.hostname);
});
it("should add defaults if there are are not enough links", async () => {
links = [{url: "foo.com"}];
const result = await feed.getLinksWithDefaults();
assert.deepEqual(result, [{url: "foo.com"}, ...DEFAULT_TOP_SITES]);
const reference = [...links, ...DEFAULT_TOP_SITES].map(s => Object.assign({}, s, {hostname: shortURLStub(s)}));
assert.deepEqual(result, reference);
});
it("should only add defaults up to TOP_SITES_SHOWMORE_LENGTH", async () => {
links = new Array(TOP_SITES_SHOWMORE_LENGTH - 1).fill({url: "foo.com"});
links = [];
for (let i = 0; i < TOP_SITES_SHOWMORE_LENGTH - 1; i++) {
links.push({url: `foo${i}.com`});
}
const result = await feed.getLinksWithDefaults();
const reference = [...links, DEFAULT_TOP_SITES[0]].map(s => Object.assign({}, s, {hostname: shortURLStub(s)}));
assert.lengthOf(result, TOP_SITES_SHOWMORE_LENGTH);
assert.deepEqual(result, [...links, DEFAULT_TOP_SITES[0]]);
assert.deepEqual(result, reference);
});
it("should not throw if NewTabUtils returns null", () => {
links = null;
@ -104,14 +140,60 @@ describe("Top Sites Feed", () => {
feed.getLinksWithDefaults(action);
});
});
describe("deduping", () => {
beforeEach(() => {
({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
"common/Reducers.jsm": {insertPinned},
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot}
}));
feed = new TopSitesFeed();
});
it("should not dedupe pinned sites", async () => {
fakeNewTabUtils.pinnedLinks.links = [
{url: "https://developer.mozilla.org/en-US/docs/Web"},
{url: "https://developer.mozilla.org/en-US/docs/Learn"}
];
const sites = await feed.getLinksWithDefaults();
assert.lengthOf(sites, 12);
assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url);
assert.equal(sites[0].hostname, sites[1].hostname);
});
it("should not dedupe pinned sites", async () => {
fakeNewTabUtils.pinnedLinks.links = [
{url: "https://developer.mozilla.org/en-US/docs/Web"},
{url: "https://developer.mozilla.org/en-US/docs/Learn"}
];
// These will be the frecent results.
links = [
{url: "https://developer.mozilla.org/en-US/docs/Web"},
{url: "https://developer.mozilla.org/en-US/docs/Learn"}
];
const sites = await feed.getLinksWithDefaults();
// Frecent results are removed and only pinned are kept.
assert.lengthOf(sites, 2);
});
it("should check against null entries", async () => {
fakeNewTabUtils.pinnedLinks.links = [null];
await feed.getLinksWithDefaults();
});
});
});
describe("#refresh", () => {
it("should dispatch an action with the links returned", async () => {
sandbox.stub(feed, "getScreenshot");
await feed.refresh(action);
const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
assert.calledOnce(feed.store.dispatch);
assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, links);
assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, reference);
});
it("should reuse screenshots for existing links, and call feed.getScreenshot for others", async () => {
sandbox.stub(feed, "getScreenshot");
@ -136,6 +218,17 @@ describe("Top Sites Feed", () => {
await feed.refresh(action);
assert.calledOnce(feed.store.dispatch);
});
it("should skip getting screenshot if there is a tippy top icon", async () => {
sandbox.stub(feed, "getScreenshot");
feed._tippyTopProvider.processSite = site => {
site.tippyTopIcon = "icon.png";
site.backgroundColor = "#fff";
return site;
};
await feed.refresh(action);
assert.calledOnce(feed.store.dispatch);
assert.notCalled(feed.getScreenshot);
});
});
describe("getScreenshot", () => {
it("should call Screenshots.getScreenshotForURL with the right url", async () => {
@ -146,18 +239,6 @@ describe("Top Sites Feed", () => {
});
describe("#onAction", () => {
const newTabAction = {type: at.NEW_TAB_LOAD, meta: {fromTarget: "target"}};
it("should call refresh if there are not enough sites on NEW_TAB_LOAD", () => {
feed.store.getState = function() { return {TopSites: {rows: []}}; };
sinon.stub(feed, "refresh");
feed.onAction(newTabAction);
assert.calledWith(feed.refresh, newTabAction.meta.fromTarget);
});
it("should call refresh if there are not sites on NEW_TAB_LOAD, not counting defaults", () => {
feed.store.getState = function() { return {TopSites: {rows: [{url: "foo.com"}, ...DEFAULT_TOP_SITES]}}; };
sinon.stub(feed, "refresh");
feed.onAction(newTabAction);
assert.calledWith(feed.refresh, newTabAction.meta.fromTarget);
});
it("should not call refresh if there are enough sites on NEW_TAB_LOAD", () => {
feed.lastUpdated = Date.now();
sinon.stub(feed, "refresh");

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

@ -14,6 +14,7 @@ describe("Top Stories Feed", () => {
let instance;
let clock;
let globals;
let shortURLStub;
beforeEach(() => {
FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
@ -32,7 +33,12 @@ describe("Top Stories Feed", () => {
globals.set("Services", {locale: {getRequestedLocale: () => "en-CA"}});
clock = sinon.useFakeTimers();
({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID, FEED_PREF, SECTION_OPTIONS_PREF} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}}));
shortURLStub = sinon.stub().callsFake(site => site.url);
({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID, FEED_PREF, SECTION_OPTIONS_PREF} = injector({
"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
"common/ShortURL.jsm": {shortURL: shortURLStub}
}));
instance = new TopStoriesFeed();
instance.store = {getState() { return {}; }, dispatch: sinon.spy()};
instance.storiesLastUpdated = 0;
@ -161,7 +167,8 @@ describe("Top Stories Feed", () => {
"image": "image-url",
"referrer": "referrer",
"url": "rec-url",
"eTLD": ""
"eTLD": "",
"hostname": "rec-url"
}];
instance.stories_endpoint = "stories-endpoint";
@ -170,6 +177,7 @@ describe("Top Stories Feed", () => {
await instance.fetchStories();
assert.calledOnce(fetchStub);
assert.calledOnce(shortURLStub);
assert.calledWithExactly(fetchStub, instance.stories_endpoint);
assert.calledOnce(instance.store.dispatch);
assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_ROWS_UPDATE);

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

@ -28,6 +28,10 @@ overrider.set({
fetch() {},
Preferences: FakePrefs,
Services: {
telemetry: {
canRecordBase: true,
canRecordExtended: true
},
locale: {
getAppLocalesAsLangTags() {},
getRequestedLocale() {},