Bug 1399226 - Add high-res icons, image placeholders and bug fixes to Activity Stream. r=dmose

MozReview-Commit-ID: HlYR6LZsM5G

--HG--
extra : rebase_source : 4551dbbbd4572070c68964e33447d2c3282a0ec4
This commit is contained in:
Ed Lee 2017-09-12 16:00:14 -07:00
Родитель 6e873f3700
Коммит ad67c33d00
54 изменённых файлов: 679 добавлений и 147 удалений

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

@ -39,6 +39,7 @@ for (const type of [
"NEW_TAB_INIT",
"NEW_TAB_INITIAL_STATE",
"NEW_TAB_LOAD",
"NEW_TAB_REHYDRATED",
"NEW_TAB_STATE_REQUEST",
"NEW_TAB_UNLOAD",
"OPEN_LINK",
@ -61,6 +62,7 @@ for (const type of [
"SECTION_ENABLE",
"SECTION_REGISTER",
"SECTION_UPDATE",
"SECTION_UPDATE_CARD",
"SET_PREF",
"SHOW_FIREFOX_ACCOUNTS",
"SNIPPETS_DATA",

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

@ -2,7 +2,9 @@
"use strict";
/* istanbul ignore if */
if (typeof Components !== "undefined" && Components.utils) {
// Note: normally we would just feature detect Components.utils here, but
// unfortunately that throws an ugly warning in content if we do.
if (typeof Window === "undefined" && typeof Components !== "undefined" && Components.utils) {
Components.utils.import("resource://gre/modules/Services.jsm");
}

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

@ -1,32 +1,73 @@
const prefConfig = {
// Prefs listed with "invalidates: true" will prevent the prerendered version
class _PrerenderData {
constructor(options) {
this.initialPrefs = options.initialPrefs;
this.initialSections = options.initialSections;
this._setValidation(options.validation);
}
get validation() {
return this._validation;
}
set validation(value) {
this._setValidation(value);
}
get invalidatingPrefs() {
return this._invalidatingPrefs;
}
// This is needed so we can use it in the constructor
_setValidation(value = []) {
this._validation = value;
this._invalidatingPrefs = value.reduce((result, next) => {
if (typeof next === "string") {
result.push(next);
return result;
} else if (next && next.oneOf) {
return result.concat(next.oneOf);
}
throw new Error("Your validation configuration is not properly configured");
}, []);
}
arePrefsValid(getPref) {
for (const prefs of this.validation) {
// {oneOf: ["foo", "bar"]}
if (prefs && prefs.oneOf && !prefs.oneOf.some(name => getPref(name) === this.initialPrefs[name])) {
return false;
// "foo"
} else if (getPref(prefs) !== this.initialPrefs[prefs]) {
return false;
}
}
return true;
}
}
this.PrerenderData = new _PrerenderData({
initialPrefs: {
"migrationExpired": true,
"showTopSites": true,
"showSearch": true,
"topSitesCount": 6,
"feeds.section.topstories": true,
"feeds.section.highlights": true
},
// Prefs listed as invalidating will prevent the prerendered version
// of AS from being used if their value is something other than what is listed
// here. This is required because some preferences cause the page layout to be
// too different for the prerendered version to be used. Unfortunately, this
// will result in users who have modified some of their preferences not being
// able to get the benefits of prerendering.
"migrationExpired": {value: true},
"showTopSites": {
value: true,
invalidates: true
},
"showSearch": {
value: true,
invalidates: true
},
"topSitesCount": {value: 6},
"feeds.section.topstories": {
value: true,
invalidates: true
}
};
this.PrerenderData = {
invalidatingPrefs: Object.keys(prefConfig).filter(key => prefConfig[key].invalidates),
initialPrefs: Object.keys(prefConfig).reduce((obj, key) => {
obj[key] = prefConfig[key].value;
return obj;
}, {}), // This creates an object of the form {prefName: value}
validation: [
"showTopSites",
"showSearch",
// This means if either of these are set to their default values,
// prerendering can be used.
{oneOf: ["feeds.section.topstories", "feeds.section.highlights"]}
],
initialSections: [
{
enabled: true,
@ -35,8 +76,16 @@ this.PrerenderData = {
order: 1,
title: {id: "header_recommended_by", values: {provider: "Pocket"}},
topics: [{}]
},
{
enabled: true,
id: "highlights",
icon: "highlights",
order: 2,
title: {id: "header_highlights"}
}
]
};
});
this.EXPORTED_SYMBOLS = ["PrerenderData"];
this._PrerenderData = _PrerenderData;
this.EXPORTED_SYMBOLS = ["PrerenderData", "_PrerenderData"];

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

@ -233,6 +233,19 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
}
return section;
});
case at.SECTION_UPDATE_CARD:
return prevState.map(section => {
if (section && section.id === action.data.id && section.rows) {
const newRows = section.rows.map(card => {
if (card.url === action.data.url) {
return Object.assign({}, card, action.data.options);
}
return card;
});
return Object.assign({}, section, {rows: newRows});
}
return section;
});
case at.PLACES_BOOKMARK_ADDED:
if (!action.data) {
return prevState;

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

@ -113,7 +113,8 @@
"showTopSites": true,
"showSearch": true,
"topSitesCount": 6,
"feeds.section.topstories": true
"feeds.section.topstories": true,
"feeds.section.highlights": true
}
},
"Dialog": {
@ -137,6 +138,17 @@
{}
],
"initialized": false
},
{
"title": {
"id": "header_highlights"
},
"rows": [],
"order": 2,
"enabled": true,
"id": "highlights",
"icon": "highlights",
"initialized": false
}
]
};

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -94,7 +94,7 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS :
// UNINIT: "UNINIT"
// }
const actionTypes = {};
for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_REGISTER", "SECTION_UPDATE", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
actionTypes[type] = type;
}
@ -583,6 +583,19 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
}
return section;
});
case at.SECTION_UPDATE_CARD:
return prevState.map(section => {
if (section && section.id === action.data.id && section.rows) {
const newRows = section.rows.map(card => {
if (card.url === action.data.url) {
return Object.assign({}, card, action.data.options);
}
return card;
});
return Object.assign({}, section, { rows: newRows });
}
return section;
});
case at.PLACES_BOOKMARK_ADDED:
if (!action.data) {
return prevState;
@ -657,8 +670,10 @@ module.exports = {
/* istanbul ignore if */
// Note: normally we would just feature detect Components.utils here, but
// unfortunately that throws an ugly warning in content if we do.
if (typeof Components !== "undefined" && Components.utils) {
if (typeof Window === "undefined" && typeof Components !== "undefined" && Components.utils) {
Components.utils.import("resource://gre/modules/Services.jsm");
}
@ -1046,10 +1061,10 @@ module.exports._unconnected = LinkMenu;
const ReactDOM = __webpack_require__(11);
const Base = __webpack_require__(12);
const { Provider } = __webpack_require__(3);
const initStore = __webpack_require__(28);
const initStore = __webpack_require__(29);
const { reducers } = __webpack_require__(6);
const DetectUserSessionStart = __webpack_require__(30);
const { addSnippetsSubscriber } = __webpack_require__(31);
const DetectUserSessionStart = __webpack_require__(31);
const { addSnippetsSubscriber } = __webpack_require__(32);
const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
new DetectUserSessionStart().sendEventOrAddListener();
@ -1092,6 +1107,7 @@ const ManualMigration = __webpack_require__(22);
const PreferencesPane = __webpack_require__(23);
const Sections = __webpack_require__(24);
const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
const { PrerenderData } = __webpack_require__(28);
// Add the locale data for pluralization and relative-time formatting for now,
// this just uses english locale data. We can make this more sophisticated if
@ -1103,6 +1119,10 @@ function addLocaleDataForReactIntl({ locale, textDirection }) {
}
class Base extends React.Component {
componentWillMount() {
this.sendNewTabRehydrated(this.props.App);
}
componentDidMount() {
// Request state AFTER the first render to ensure we don't cause the
// prerendered DOM to be unmounted. Otherwise, NEW_TAB_STATE_REQUEST is
@ -1117,7 +1137,10 @@ class Base extends React.Component {
document.getElementById("favicon").href += "#";
}, { once: true });
}
componentWillUpdate({ App }) {
this.sendNewTabRehydrated(App);
// Early loads might not have locale yet, so wait until we do
if (App.locale && App.locale !== this.props.App.locale) {
addLocaleDataForReactIntl(App);
@ -1131,11 +1154,25 @@ class Base extends React.Component {
}
}
// The NEW_TAB_REHYDRATED event is used to inform feeds that their
// data has been consumed e.g. for counting the number of tabs that
// have rendered that data.
sendNewTabRehydrated(App) {
if (App && App.initialized && !this.renderNotified) {
this.props.dispatch(ac.SendToMain({ type: at.NEW_TAB_REHYDRATED, data: {} }));
this.renderNotified = true;
}
}
render() {
const props = this.props;
const { locale, strings, initialized } = props.App;
const prefs = props.Prefs.values;
const shouldBeFixedToTop = PrerenderData.arePrefsValid(name => prefs[name]);
const outerClassName = `outer-wrapper${shouldBeFixedToTop ? " fixed-to-top" : ""}`;
if (!props.isPrerendered && !initialized) {
return null;
}
@ -1148,7 +1185,7 @@ class Base extends React.Component {
{ key: "STATIC", locale: locale, messages: strings },
React.createElement(
"div",
{ className: "outer-wrapper" },
{ className: outerClassName },
React.createElement(
"main",
null,
@ -1165,6 +1202,7 @@ class Base extends React.Component {
}
module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base);
module.exports._unconnected = Base;
/***/ }),
/* 13 */
@ -2642,6 +2680,8 @@ class Card extends React.Component {
const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
// Display "now" as "trending" until we have new strings #3402
const { icon, intlID } = cardContextTypes[link.type === "now" ? "trending" : link.type] || {};
const hasImage = link.image || link.hasImage;
const imageStyle = { backgroundImage: link.image ? `url(${link.image})` : "none" };
return React.createElement(
"li",
@ -2652,10 +2692,14 @@ class Card extends React.Component {
React.createElement(
"div",
{ className: "card" },
link.image && React.createElement("div", { className: "card-preview-image", style: { backgroundImage: `url(${link.image})` } }),
hasImage && React.createElement(
"div",
{ className: "card-preview-image-outer" },
React.createElement("div", { className: `card-preview-image${link.image ? " loaded" : ""}`, style: imageStyle })
),
React.createElement(
"div",
{ className: `card-details${link.image ? "" : " no-image"}` },
{ className: `card-details${hasImage ? "" : " no-image"}` },
link.hostname && React.createElement(
"div",
{ className: "card-host-name" },
@ -2663,7 +2707,7 @@ class Card extends React.Component {
),
React.createElement(
"div",
{ className: `card-text${link.image ? "" : " no-image"}${link.hostname ? "" : " no-host-name"}${icon ? "" : " no-context"}` },
{ className: `card-text${hasImage ? "" : " no-image"}${link.hostname ? "" : " no-host-name"}${icon ? "" : " no-context"}` },
React.createElement(
"h4",
{ className: "card-title", dir: "auto" },
@ -2792,11 +2836,102 @@ module.exports.Topic = Topic;
/***/ }),
/* 28 */
/***/ (function(module, exports) {
class _PrerenderData {
constructor(options) {
this.initialPrefs = options.initialPrefs;
this.initialSections = options.initialSections;
this._setValidation(options.validation);
}
get validation() {
return this._validation;
}
set validation(value) {
this._setValidation(value);
}
get invalidatingPrefs() {
return this._invalidatingPrefs;
}
// This is needed so we can use it in the constructor
_setValidation(value = []) {
this._validation = value;
this._invalidatingPrefs = value.reduce((result, next) => {
if (typeof next === "string") {
result.push(next);
return result;
} else if (next && next.oneOf) {
return result.concat(next.oneOf);
}
throw new Error("Your validation configuration is not properly configured");
}, []);
}
arePrefsValid(getPref) {
for (const prefs of this.validation) {
// {oneOf: ["foo", "bar"]}
if (prefs && prefs.oneOf && !prefs.oneOf.some(name => getPref(name) === this.initialPrefs[name])) {
return false;
// "foo"
} else if (getPref(prefs) !== this.initialPrefs[prefs]) {
return false;
}
}
return true;
}
}
var PrerenderData = new _PrerenderData({
initialPrefs: {
"migrationExpired": true,
"showTopSites": true,
"showSearch": true,
"topSitesCount": 6,
"feeds.section.topstories": true,
"feeds.section.highlights": true
},
// Prefs listed as invalidating will prevent the prerendered version
// of AS from being used if their value is something other than what is listed
// here. This is required because some preferences cause the page layout to be
// too different for the prerendered version to be used. Unfortunately, this
// will result in users who have modified some of their preferences not being
// able to get the benefits of prerendering.
validation: ["showTopSites", "showSearch",
// This means if either of these are set to their default values,
// prerendering can be used.
{ oneOf: ["feeds.section.topstories", "feeds.section.highlights"] }],
initialSections: [{
enabled: true,
icon: "pocket",
id: "topstories",
order: 1,
title: { id: "header_recommended_by", values: { provider: "Pocket" } },
topics: [{}]
}, {
enabled: true,
id: "highlights",
icon: "highlights",
order: 2,
title: { id: "header_highlights" }
}]
});
module.exports = {
PrerenderData,
_PrerenderData
};
/***/ }),
/* 29 */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {/* eslint-env mozilla/frame-script */
const { createStore, combineReducers, applyMiddleware } = __webpack_require__(29);
const { createStore, combineReducers, applyMiddleware } = __webpack_require__(30);
const { actionTypes: at, actionCreators: ac, actionUtils: au } = __webpack_require__(0);
const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
@ -2906,13 +3041,13 @@ module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 29 */
/* 30 */
/***/ (function(module, exports) {
module.exports = Redux;
/***/ }),
/* 30 */
/* 31 */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {const { actionTypes: at } = __webpack_require__(0);
@ -2982,7 +3117,7 @@ module.exports = class DetectUserSessionStart {
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 31 */
/* 32 */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {const DATABASE_NAME = "snippets_db";

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

@ -168,9 +168,11 @@ a {
.outer-wrapper {
display: flex;
flex-grow: 1;
padding: 40px 32px 32px;
height: 100%; }
height: 100%;
flex-grow: 1; }
.outer-wrapper.fixed-to-top {
height: auto; }
main {
margin: auto;
@ -976,14 +978,30 @@ main {
opacity: 1; }
.card-outer:hover .card-title, .card-outer:focus .card-title, .card-outer.active .card-title {
color: #0060DF; }
.card-outer .card-preview-image {
.card-outer .card-preview-image-outer {
position: relative;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background: linear-gradient(135deg, #B1B1B3, #D7D7DB);
height: 122px;
border-bottom: 1px solid #D7D7DB;
border-radius: 3px 3px 0 0; }
border-radius: 3px 3px 0 0;
overflow: hidden; }
.card-outer .card-preview-image-outer:dir(rtl) {
background: linear-gradient(225deg, #B1B1B3, #D7D7DB); }
.card-outer .card-preview-image-outer::after {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
bottom: 0;
content: " ";
position: absolute;
width: 100%; }
.card-outer .card-preview-image-outer .card-preview-image {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transition: opacity 1s;
opacity: 0; }
.card-outer .card-preview-image-outer .card-preview-image.loaded {
opacity: 1; }
.card-outer .card-details {
padding: 15px 16px 12px; }
.card-outer .card-details.no-image {

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

До

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

После

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

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

До

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

После

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

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

До

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

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

До

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

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

До

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

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

До

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

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

До

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

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

После

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

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

После

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

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

До

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

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

До

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

После

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

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

До

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

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

До

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

После

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

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

До

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

После

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

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

После

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

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

До

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

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

После

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

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

До

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

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

До

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

После

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

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

До

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

После

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

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

До

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

После

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

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

До

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

После

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

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

До

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

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

После

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

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

После

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

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

До

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

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

До

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

После

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

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

@ -11,33 +11,13 @@
},
{
"title": "amazon",
"url": "https://www.amazon.ca/",
"image_url": "amazon-ca@2x.png"
},
{
"title": "amazon",
"url": "https://www.amazon.co.uk/",
"image_url": "amazon-uk@2x.png"
},
{
"title": "amazon",
"url": "https://www.amazon.com/",
"image_url": "amazon-com@2x.png"
},
{
"title": "amazon",
"url": "https://www.amazon.de/",
"image_url": "amazon-de@2x.png"
},
{
"title": "amazon",
"url": "https://www.amazon.fr/",
"image_url": "amazon-fr@2x.png"
"urls": ["https://www.amazon.ca/", "https://www.amazon.co.uk/", "https://www.amazon.com/", "https://www.amazon.de/", "https://www.amazon.fr/"],
"image_url": "amazon@2x.png"
},
{
"title": "avito",
"url": "https://www.avito.ru/",
"image_url": "avito@2x.png"
"image_url": "avito-ru@2x.png"
},
{
"title": "bbc",
@ -46,14 +26,9 @@
},
{
"title": "ebay",
"urls": ["https://www.ebay.com", "https://www.ebay.co.uk/"],
"urls": ["https://www.ebay.com", "https://www.ebay.co.uk/", "https://ebay.de"],
"image_url": "ebay@2x.png"
},
{
"title": "ebay",
"url": "https://ebay.de",
"image_url": "ebay-de@2x.png"
},
{
"title": "facebook",
"url": "https://www.facebook.com/",
@ -62,12 +37,12 @@
{
"title": "leboncoin",
"url": "http://www.leboncoin.fr/",
"image_url": "leboncoin@2x.png"
"image_url": "leboncoin-fr@2x.png"
},
{
"title": "ok",
"url": "https://www.ok.ru/",
"image_url": "ok@2x.png"
"image_url": "ok-ru@2x.png"
},
{
"title": "olx",
@ -92,12 +67,12 @@
{
"title": "wikipedia",
"url": "https://www.wikipedia.org/",
"image_url": "wikipedia-com@2x.png"
"image_url": "wikipedia-org@2x.png"
},
{
"title": "wykop",
"url": "https://www.wykop.pl/",
"image_url": "wykop@2x.png"
"image_url": "wykop-pl@2x.png"
},
{
"title": "youtube",

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

@ -5997,8 +5997,8 @@
"header_recommended_by": "{provider} öneriyor",
"header_bookmarks_placeholder": "Henüz hiç yer iminiz yok.",
"header_stories_from": "kaynak:",
"type_label_visited": "Ziyaret edildi",
"type_label_bookmarked": "Yer imlerine eklendi",
"type_label_visited": "Ziyaret etmiştiniz",
"type_label_bookmarked": "Yer imlerinizde",
"type_label_synced": "Başka bir cihazdan eşitlendi",
"type_label_recommended": "Popüler",
"type_label_open": "Açık",
@ -6075,7 +6075,7 @@
"pocket_description": "Mozilla ailesinin yeni üyesi Pocketın yardımıyla, gözünüzden kaçabilecek kaliteli içerikleri keşfedin.",
"highlights_empty_state": "Gezinmeye başlayın. Son zamanlarda baktığınız veya yer imlerinize eklediğiniz bazı güzel makaleleri, videoları ve diğer sayfaları burada göstereceğiz.",
"topstories_empty_state": "Hepsini bitirdiniz. Yeni {provider} haberleri için daha fazla yine gelin. Beklemek istemiyor musunuz? İlginç yazılara ulaşmak için popüler konulardan birini seçebilirsiniz.",
"manual_migration_explanation2": "Öteki tarayıcılarınızdaki yer işaretlerinizi, geçmişinizi ve parolalarınızı Firefoxa taşıyabilirsiniz.",
"manual_migration_explanation2": "Öteki tarayıcılarınızdaki yer imlerinizi, geçmişinizi ve parolalarınızı Firefoxa aktarabilirsiniz.",
"manual_migration_cancel_button": "Gerek yok",
"manual_migration_import_button": "Olur, aktaralım"
},

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

@ -8,7 +8,7 @@
<em:type>2</em:type>
<em:bootstrap>true</em:bootstrap>
<em:unpack>false</em:unpack>
<em:version>2017.09.11.1306-373d9fc</em:version>
<em:version>2017.09.12.1376-781e5de5</em:version>
<em:name>Activity Stream</em:name>
<em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
<em:multiprocessCompatible>true</em:multiprocessCompatible>

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

@ -60,7 +60,8 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
*/
middleware(store) {
return next => action => {
if (!this.channel) {
const skipMain = action.meta && action.meta.skipMain;
if (!this.channel && !skipMain) {
next(action);
return;
}
@ -69,7 +70,10 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
} else if (au.isBroadcastToContent(action)) {
this.broadcast(action);
}
next(action);
if (!skipMain) {
next(action);
}
};
}
@ -136,10 +140,10 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
this.channel.addMessageListener(this.incomingMessageName, this.onMessage);
// Some pages might have already loaded, so we won't get the usual message
for (const {loaded, portID} of this.channel.messagePorts) {
const simulatedMsg = {target: {portID}};
for (const target of this.channel.messagePorts) {
const simulatedMsg = {target};
this.onNewTabInit(simulatedMsg);
if (loaded) {
if (target.loaded) {
this.onNewTabLoad(simulatedMsg);
}
}
@ -168,7 +172,10 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
* @param {obj} msg The messsage from a page that was just initialized
*/
onNewTabInit(msg) {
this.onActionFromContent({type: at.NEW_TAB_INIT}, msg.target.portID);
this.onActionFromContent({
type: at.NEW_TAB_INIT,
data: {url: msg.target.url}
}, msg.target.portID);
}
/**

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

@ -15,6 +15,8 @@ const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Screenshots",
"resource://activity-stream/lib/Screenshots.jsm");
const HIGHLIGHTS_MAX_LENGTH = 9;
const HIGHLIGHTS_UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
@ -26,6 +28,7 @@ this.HighlightsFeed = class HighlightsFeed {
this.highlightsLastUpdated = 0;
this.highlights = [];
this.dedupe = new Dedupe(this._dedupeKey);
this.imageCache = new Map();
}
_dedupeKey(site) {
@ -62,10 +65,16 @@ this.HighlightsFeed = class HighlightsFeed {
continue;
}
// If we already have the image for the card in the cache, use that
// immediately. Then asynchronously fetch the image (refreshes the cache).
const image = this.imageCache.get(page.url);
this.fetchImage(page.url, page.preview_image_url);
// We want the page, so update various fields for UI
Object.assign(page, {
image,
hasImage: true, // We always have an image - fall back to a screenshot
hostname,
image: page.preview_image_url,
type: page.bookmarkGuid ? "bookmark" : page.type
});
@ -81,6 +90,23 @@ this.HighlightsFeed = class HighlightsFeed {
SectionsManager.updateSection(SECTION_ID, {rows: this.highlights}, this.highlightsLastUpdated === 0 || broadcast);
this.highlightsLastUpdated = Date.now();
// Clearing the image cache here is okay. The asynchronous fetchImage calls
// get scheduled after the body of fetchHighlights has been executed, so they
// then fill up the cache again for the next fetchHighlights call.
this.imageCache.clear();
}
/**
* Fetch an image for a given highlight, store it in the image cache, and
* update the card with the new image. If the highlight has a preview image
* then use that, else fall back to a screenshot of the page.
*/
async fetchImage(url, imageUrl) {
const image = await Screenshots.getScreenshotForURL(imageUrl || url);
if (image) {
this.imageCache.set(url, image);
}
SectionsManager.updateSectionCard(SECTION_ID, url, {image}, true);
}
onAction(action) {

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

@ -17,14 +17,8 @@ this.PrefsFeed = class PrefsFeed {
// If the any prefs are set to something other than what the prerendered version
// of AS expects, we can't use it.
_setPrerenderPref() {
for (const prefName of PrerenderData.invalidatingPrefs) {
if (this._prefs.get(prefName) !== PrerenderData.initialPrefs[prefName]) {
this._prefs.set("prerender", false);
return;
}
}
this._prefs.set("prerender", true);
_setPrerenderPref(name) {
this._prefs.set("prerender", PrerenderData.arePrefsValid(pref => this._prefs.get(pref)));
}
_checkPrerender(name) {
@ -37,7 +31,7 @@ this.PrefsFeed = class PrefsFeed {
if (this._prefMap.has(name)) {
this.store.dispatch(ac.BroadcastToContent({type: at.PREF_CHANGED, data: {name, value}}));
}
this._checkPrerender(name, value);
this._checkPrerender(name);
}
init() {

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

@ -19,6 +19,8 @@ XPCOMUtils.defineLazyServiceGetter(this, "MIMEService",
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
const GREY_10 = "#F9F9FA";
this.Screenshots = {
/**
* Convert bytes to a string using extremely fast String.fromCharCode without
@ -41,7 +43,7 @@ this.Screenshots = {
async getScreenshotForURL(url) {
let screenshot = null;
try {
await BackgroundPageThumbs.captureIfMissing(url);
await BackgroundPageThumbs.captureIfMissing(url, {backgroundColor: GREY_10});
const imgPath = PageThumbs.getThumbnailPath(url);
// OS.File object used to easily read off-thread

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

@ -139,6 +139,26 @@ const SectionsManager = {
o => !this.CONTEXT_MENU_PREFS[o] || Services.prefs.getBoolPref(this.CONTEXT_MENU_PREFS[o]));
}
},
/**
* Update a specific section card by its url. This allows an action to be
* broadcast to all existing pages to update a specific card without having to
* also force-update the rest of the section's cards and state on those pages.
*
* @param id The id of the section with the card to be updated
* @param url The url of the card to update
* @param options The options to update for the card
* @param shouldBroadcast Whether or not to broadcast the update
*/
updateSectionCard(id, url, options, shouldBroadcast) {
if (this.sections.has(id)) {
const card = this.sections.get(id).rows.find(elem => elem.url === url);
if (card) {
Object.assign(card, options);
}
this.emit(this.UPDATE_SECTION_CARD, id, url, options, shouldBroadcast);
}
},
onceInitialized(callback) {
if (this.initialized) {
callback();
@ -160,6 +180,7 @@ for (const action of [
"ENABLE_SECTION",
"DISABLE_SECTION",
"UPDATE_SECTION",
"UPDATE_SECTION_CARD",
"INIT",
"UNINIT"
]) {
@ -174,12 +195,14 @@ class SectionsFeed {
this.onAddSection = this.onAddSection.bind(this);
this.onRemoveSection = this.onRemoveSection.bind(this);
this.onUpdateSection = this.onUpdateSection.bind(this);
this.onUpdateSectionCard = this.onUpdateSectionCard.bind(this);
}
init() {
SectionsManager.on(SectionsManager.ADD_SECTION, this.onAddSection);
SectionsManager.on(SectionsManager.REMOVE_SECTION, this.onRemoveSection);
SectionsManager.on(SectionsManager.UPDATE_SECTION, this.onUpdateSection);
SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, this.onUpdateSectionCard);
// Catch any sections that have already been added
SectionsManager.sections.forEach((section, id) =>
this.onAddSection(SectionsManager.ADD_SECTION, id, section));
@ -191,6 +214,7 @@ class SectionsFeed {
SectionsManager.off(SectionsManager.ADD_SECTION, this.onAddSection);
SectionsManager.off(SectionsManager.REMOVE_SECTION, this.onRemoveSection);
SectionsManager.off(SectionsManager.UPDATE_SECTION, this.onUpdateSection);
SectionsManager.off(SectionsManager.UPDATE_SECTION_CARD, this.onUpdateSectionCard);
}
onAddSection(event, id, options) {
@ -210,6 +234,13 @@ class SectionsFeed {
}
}
onUpdateSectionCard(event, id, url, options, shouldBroadcast = false) {
if (options) {
const action = {type: at.SECTION_UPDATE_CARD, data: {id, url, options}};
this.store.dispatch(shouldBroadcast ? ac.BroadcastToContent(action) : action);
}
}
onAction(action) {
switch (action.type) {
case at.INIT:

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

@ -189,17 +189,21 @@ this.TelemetryFeed = class TelemetryFeed {
* addSession - Start tracking a new session
*
* @param {string} id the portID of the open session
*
* @param {string} the URL being loaded for this session (optional)
* @return {obj} Session object
*/
addSession(id) {
addSession(id, url) {
const session = {
session_id: String(gUUIDGenerator.generateUUID()),
page: "about:newtab", // TODO: Handle about:home here
// "unknown" will be overwritten when appropriate
page: url ? url : "unknown",
// "unexpected" will be overwritten when appropriate
perf: {load_trigger_type: "unexpected"}
};
if (url) {
session.page = url;
}
this.sessions.set(id, session);
return session;
}
@ -242,10 +246,11 @@ this.TelemetryFeed = class TelemetryFeed {
// If the ping is part of a user session, add session-related info
if (portID) {
const session = this.sessions.get(portID) || this.addSession(portID);
Object.assign(ping, {
session_id: session.session_id,
page: session.page
});
Object.assign(ping, {session_id: session.session_id});
if (session.page) {
Object.assign(ping, {page: session.page});
}
}
return ping;
}
@ -355,7 +360,7 @@ this.TelemetryFeed = class TelemetryFeed {
this.init();
break;
case at.NEW_TAB_INIT:
this.addSession(au.getPortIdOfSender(action));
this.addSession(au.getPortIdOfSender(action), action.data.url);
break;
case at.NEW_TAB_UNLOAD:
this.endSession(au.getPortIdOfSender(action));

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

@ -57,7 +57,7 @@ this.TopSitesFeed = class TopSitesFeed {
let frecent = await NewTabUtils.activityStreamLinks.getTopSites();
const notBlockedDefaultSites = DEFAULT_TOP_SITES.filter(site => !NewTabUtils.blockedLinks.isBlocked({url: site.url}));
const defaultUrls = notBlockedDefaultSites.map(site => site.url);
let pinned = NewTabUtils.pinnedLinks.links;
let pinned = this._getPinnedWithData(frecent);
pinned = pinned.map(site => site && Object.assign({}, site, {
isDefault: defaultUrls.indexOf(site.url) !== -1,
hostname: shortURL(site)
@ -122,14 +122,18 @@ this.TopSitesFeed = class TopSitesFeed {
}
this.lastUpdated = Date.now();
}
_getPinnedWithData() {
// Augment the pinned links with any other extra data we have for them already in the store
const links = this.store.getState().TopSites.rows;
_getPinnedWithData(links) {
// Augment the pinned links with any other extra data we have for them already in the store.
// Alternatively you can pass in some links that you know have data you want the pinned links
// to also have. This is useful for start up to make sure pinned links have favicons
// (See github ticket #3428 fore more details)
let originalLinks = links ? links : this.store.getState().TopSites.rows;
const pinned = NewTabUtils.pinnedLinks.links;
return pinned.map(pinnedLink => {
if (pinnedLink) {
const hostname = shortURL(pinnedLink);
return Object.assign(links.find(link => link && link.url === pinnedLink.url) || {hostname}, pinnedLink);
const originalLink = originalLinks.find(link => link && link.url === pinnedLink.url);
return Object.assign(pinnedLink, originalLink || {hostname});
}
return pinnedLink;
});

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

@ -7,7 +7,7 @@ const baseKeys = {
addon_version: Joi.string().required(),
locale: Joi.string().required(),
session_id: Joi.string(),
page: Joi.valid(["about:home", "about:newtab"]),
page: Joi.valid(["about:home", "about:newtab", "unknown"]),
user_prefs: Joi.number().integer().required()
};

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

@ -0,0 +1,98 @@
const {PrerenderData, _PrerenderData} = require("common/PrerenderData.jsm");
describe("_PrerenderData", () => {
describe("properties", () => {
it("should set .initialPrefs", () => {
const initialPrefs = {foo: true};
const instance = new _PrerenderData({initialPrefs});
assert.equal(instance.initialPrefs, initialPrefs);
});
it("should set .initialSections", () => {
const initialSections = [{id: "foo"}];
const instance = new _PrerenderData({initialSections});
assert.equal(instance.initialSections, initialSections);
});
it("should set .validation and .invalidatingPrefs in the constructor", () => {
const validation = ["foo", "bar", {oneOf: ["baz", "qux"]}];
const instance = new _PrerenderData({validation});
assert.equal(instance.validation, validation);
assert.deepEqual(instance.invalidatingPrefs, ["foo", "bar", "baz", "qux"]);
});
it("should also set .invalidatingPrefs when .validation is set", () => {
const validation = ["foo", "bar", {oneOf: ["baz", "qux"]}];
const instance = new _PrerenderData({validation});
const newValidation = ["foo", {oneOf: ["blah", "gloo"]}];
instance.validation = newValidation;
assert.equal(instance.validation, newValidation);
assert.deepEqual(instance.invalidatingPrefs, ["foo", "blah", "gloo"]);
});
it("should throw if an invalid validation config is set", () => {
// {stuff: []} is not a valid configuration type
assert.throws(() => new _PrerenderData({validation: ["foo", {stuff: ["bar"]}]}));
});
});
describe("#arePrefsValid", () => {
let FAKE_PREFS;
const getPrefs = pref => FAKE_PREFS[pref];
beforeEach(() => {
FAKE_PREFS = {};
});
it("should return true if all prefs match", () => {
FAKE_PREFS = {foo: true, bar: false};
const instance = new _PrerenderData({
initialPrefs: FAKE_PREFS,
validation: ["foo", "bar"]
});
assert.isTrue(instance.arePrefsValid(getPrefs));
});
it("should return true if all *invalidating* prefs match", () => {
FAKE_PREFS = {foo: true, bar: false};
const instance = new _PrerenderData({
initialPrefs: {foo: true, bar: true},
validation: ["foo"]
});
assert.isTrue(instance.arePrefsValid(getPrefs));
});
it("should return true if one each oneOf group matches", () => {
FAKE_PREFS = {foo: false, floo: true, bar: false, blar: true};
const instance = new _PrerenderData({
initialPrefs: {foo: true, floo: true, bar: true, blar: true},
validation: [{oneOf: ["foo", "floo"]}, {oneOf: ["bar", "blar"]}]
});
assert.isTrue(instance.arePrefsValid(getPrefs));
});
it("should return false if an invalidating pref is mismatched", () => {
FAKE_PREFS = {foo: true, bar: false};
const instance = new _PrerenderData({
initialPrefs: {foo: true, bar: true},
validation: ["foo", "bar"]
});
assert.isFalse(instance.arePrefsValid(getPrefs));
});
it("should return false if none of the oneOf group matches", () => {
FAKE_PREFS = {foo: true, bar: false, baz: false};
const instance = new _PrerenderData({
initialPrefs: {foo: true, bar: true, baz: true},
validation: ["foo", {oneOf: ["bar", "baz"]}]
});
assert.isFalse(instance.arePrefsValid(getPrefs));
});
});
});
// This is the instance used by Activity Stream
describe("PrerenderData", () => {
it("should set initial values for all invalidating prefs", () => {
PrerenderData.invalidatingPrefs.forEach(pref => {
assert.property(PrerenderData.initialPrefs, pref);
});
});
});

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

@ -328,6 +328,30 @@ describe("Reducers", () => {
const updatedSection = newState.find(section => section.id === "foo_bar_2");
assert.propertyVal(updatedSection, "initialized", true);
});
it("should have no effect on SECTION_UPDATE_CARD if the id or url doesn't exist", () => {
const noIdAction = {type: at.SECTION_UPDATE_CARD, data: {id: "non-existent", url: "www.foo.bar", options: {title: "New title"}}};
const noIdState = Sections(oldState, noIdAction);
const noUrlAction = {type: at.SECTION_UPDATE_CARD, data: {id: "foo_bar_2", url: "www.non-existent.url", options: {title: "New title"}}};
const noUrlState = Sections(oldState, noUrlAction);
assert.deepEqual(noIdState, oldState);
assert.deepEqual(noUrlState, oldState);
});
it("should update the card with the correct data on SECTION_UPDATE_CARD", () => {
const action = {type: at.SECTION_UPDATE_CARD, data: {id: "foo_bar_2", url: "www.other.url", options: {title: "Fake new title"}}};
const newState = Sections(oldState, action);
const updatedSection = newState.find(section => section.id === "foo_bar_2");
const updatedCard = updatedSection.rows.find(card => card.url === "www.other.url");
assert.propertyVal(updatedCard, "title", "Fake new title");
});
it("should only update the cards belonging to the right section on SECTION_UPDATE_CARD", () => {
const action = {type: at.SECTION_UPDATE_CARD, data: {id: "foo_bar_2", url: "www.other.url", options: {title: "Fake new title"}}};
const newState = Sections(oldState, action);
newState.forEach((section, i) => {
if (section.id !== "foo_bar_2") {
assert.deepEqual(section, oldState[i]);
}
});
});
it("should allow action.data to set .initialized", () => {
const data = {rows: [], initialized: false, id: "foo_bar_2"};
const action = {type: at.SECTION_UPDATE, data};

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

@ -82,13 +82,22 @@ describe("ActivityStreamMessageChannel", () => {
});
it("should simluate init for existing ports", () => {
sinon.stub(mm, "onActionFromContent");
RPmessagePorts.push({loaded: false, portID: "inited"});
RPmessagePorts.push({loaded: true, portID: "loaded"});
RPmessagePorts.push({
url: "about:monkeys",
loaded: false,
portID: "inited"
});
RPmessagePorts.push({
url: "about:sheep",
loaded: true,
portID: "loaded"
});
mm.createChannel();
assert.calledWith(mm.onActionFromContent.firstCall, {type: at.NEW_TAB_INIT}, "inited");
assert.calledWith(mm.onActionFromContent.secondCall, {type: at.NEW_TAB_INIT}, "loaded");
assert.calledWith(mm.onActionFromContent.firstCall, {type: at.NEW_TAB_INIT, data: {url: "about:monkeys"}}, "inited");
assert.calledWith(mm.onActionFromContent.secondCall, {type: at.NEW_TAB_INIT, data: {url: "about:sheep"}}, "loaded");
});
it("should simluate load for loaded ports", () => {
sinon.stub(mm, "onActionFromContent");
@ -146,10 +155,15 @@ describe("ActivityStreamMessageChannel", () => {
});
describe("#onNewTabInit", () => {
it("should dispatch a NEW_TAB_INIT action", () => {
const t = {portID: "foo"};
const t = {portID: "foo", url: "about:monkeys"};
sinon.stub(mm, "onActionFromContent");
mm.onNewTabInit({target: t});
assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_INIT}, "foo");
assert.calledWith(mm.onActionFromContent, {
type: at.NEW_TAB_INIT,
data: {url: "about:monkeys"}
}, "foo");
});
});
describe("#onNewTabLoad", () => {
@ -245,6 +259,17 @@ describe("ActivityStreamMessageChannel", () => {
store.dispatch({type: "ADD", data: 10});
assert.equal(store.getState(), 10);
});
it("should not call next if skipMain is true", () => {
store.dispatch({type: "ADD", data: 10, meta: {skipMain: true}});
assert.equal(store.getState(), 0);
sinon.stub(mm, "send");
const action = ac.SendToContent({type: "ADD", data: 10, meta: {skipMain: true}}, "foo");
mm.createChannel();
store.dispatch(action);
assert.calledWith(mm.send, action);
assert.equal(store.getState(), 0);
});
it("should call .send if the action is SendToContent", () => {
sinon.stub(mm, "send");
const action = ac.SendToContent({type: "FOO"}, "foo");

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

@ -5,8 +5,9 @@ const {actionTypes: at} = require("common/Actions.jsm");
const {Dedupe} = require("common/Dedupe.jsm");
const FAKE_LINKS = new Array(9).fill(null).map((v, i) => ({url: `http://www.site${i}.com`}));
const FAKE_IMAGE = "data123";
describe("Top Sites Feed", () => {
describe("Highlights Feed", () => {
let HighlightsFeed;
let HIGHLIGHTS_UPDATE_TIME;
let SECTION_ID;
@ -15,6 +16,7 @@ describe("Top Sites Feed", () => {
let sandbox;
let links;
let clock;
let fakeScreenshot;
let fakeNewTabUtils;
let sectionsManagerStub;
let shortURLStub;
@ -28,13 +30,16 @@ describe("Top Sites Feed", () => {
enableSection: sinon.spy(),
disableSection: sinon.spy(),
updateSection: sinon.spy(),
updateSectionCard: sinon.spy(),
sections: new Map([["highlights", {}]])
};
fakeScreenshot = {getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_IMAGE))};
shortURLStub = sinon.stub().callsFake(site => site.url.match(/\/([^/]+)/)[1]);
globals.set("NewTabUtils", fakeNewTabUtils);
({HighlightsFeed, HIGHLIGHTS_UPDATE_TIME, SECTION_ID} = injector({
"lib/ShortURL.jsm": {shortURL: shortURLStub},
"lib/SectionsManager.jsm": {SectionsManager: sectionsManagerStub},
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot},
"common/Dedupe.jsm": {Dedupe}
}));
feed = new HighlightsFeed();
@ -69,11 +74,24 @@ describe("Top Sites Feed", () => {
});
});
describe("#fetchHighlights", () => {
it("should add hostname and image to each link", async () => {
links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
it("should add hostname and hasImage to each link", async () => {
links = [{url: "https://mozilla.org"}];
await feed.fetchHighlights();
assert.equal(feed.highlights[0].hostname, "mozilla.org");
assert.equal(feed.highlights[0].image, links[0].preview_image_url);
assert.equal(feed.highlights[0].hasImage, true);
});
it("should add the image from the imageCache if it exists to the link", async () => {
links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
feed.imageCache = new Map([["https://mozilla.org", FAKE_IMAGE]]);
await feed.fetchHighlights();
assert.equal(feed.highlights[0].image, FAKE_IMAGE);
});
it("should call fetchImage with the correct arguments for each link", async () => {
links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
sinon.spy(feed, "fetchImage");
await feed.fetchHighlights();
assert.calledOnce(feed.fetchImage);
assert.calledWith(feed.fetchImage, links[0].url, links[0].preview_image_url);
});
it("should not include any links already in Top Sites", async () => {
links = [
@ -115,6 +133,38 @@ describe("Top Sites Feed", () => {
await feed.fetchHighlights();
assert.equal(feed.highlights[0].type, "bookmark");
});
it("should clear the imageCache at the end", async () => {
links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jpg"}];
feed.imageCache = new Map([["https://mozilla.org", FAKE_IMAGE]]);
// Stops fetchImage adding to the cache
feed.fetchImage = () => {};
await feed.fetchHighlights();
assert.equal(feed.imageCache.size, 0);
});
});
describe("#fetchImage", () => {
const FAKE_URL = "https://mozilla.org";
const FAKE_IMAGE_URL = "https://mozilla.org/preview.jpg";
it("should capture the image, if available", async () => {
await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
assert.calledOnce(fakeScreenshot.getScreenshotForURL);
assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_IMAGE_URL);
});
it("should fall back to capturing a screenshot", async () => {
await feed.fetchImage(FAKE_URL, undefined);
assert.calledOnce(fakeScreenshot.getScreenshotForURL);
assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_URL);
});
it("should store the image in the imageCache", async () => {
feed.imageCache.clear();
await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
assert.equal(feed.imageCache.get(FAKE_URL), FAKE_IMAGE);
});
it("should call SectionsManager.updateSectionCard with the right arguments", async () => {
await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
assert.calledOnce(sectionsManagerStub.updateSectionCard);
assert.calledWith(sectionsManagerStub.updateSectionCard, "highlights", FAKE_URL, {image: FAKE_IMAGE}, true);
});
});
describe("#uninit", () => {
it("should disable its section", () => {

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

@ -57,7 +57,7 @@ describe("PrefsFeed", () => {
});
it("should set prerender pref to false if a pref does not match its initial value", () => {
Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
FAKE_PREFS.set("feeds.section.topstories", false);
FAKE_PREFS.set("showSearch", false);
feed.onAction({type: at.INIT});
assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, false);
});
@ -70,16 +70,16 @@ describe("PrefsFeed", () => {
it("should set the prerender pref to false if a pref in invalidatingPrefs is changed from its original value", () => {
Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
feed._prefs.set("feeds.section.topstories", false);
feed.onPrefChanged("feeds.section.topstories", false);
feed._prefs.set("showSearch", false);
feed.onPrefChanged("showSearch", false);
assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, false);
});
it("should set the prerender pref back to true if the invalidatingPrefs are changed back to their original values", () => {
Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
FAKE_PREFS.set("feeds.section.topstories", false);
FAKE_PREFS.set("showSearch", false);
feed._prefs.set("feeds.section.topstories", true);
feed.onPrefChanged("feeds.section.topstories", true);
feed._prefs.set("showSearch", true);
feed.onPrefChanged("showSearch", true);
assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, true);
});
});

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

@ -5,7 +5,9 @@ const {MAIN_MESSAGE_TYPE, CONTENT_MESSAGE_TYPE} = require("common/Actions.jsm");
const FAKE_ID = "FAKE_ID";
const FAKE_OPTIONS = {icon: "FAKE_ICON", title: "FAKE_TITLE"};
const FAKE_ROWS = [{url: "1"}, {url: "2"}, {"url": "3"}];
const FAKE_ROWS = [{url: "1.example.com"}, {url: "2.example.com"}, {"url": "3.example.com"}];
const FAKE_URL = "2.example.com";
const FAKE_CARD_OPTIONS = {title: "Some fake title"};
describe("SectionsManager", () => {
let globals;
@ -183,6 +185,24 @@ describe("SectionsManager", () => {
assert.calledWith(SectionsManager.once, SectionsManager.INIT, callback);
});
});
describe("#updateSectionCard", () => {
it("should emit an UPDATE_SECTION_CARD event with correct arguments", () => {
SectionsManager.addSection(FAKE_ID, Object.assign({}, FAKE_OPTIONS, {rows: FAKE_ROWS}));
const spy = sinon.spy();
SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy);
SectionsManager.updateSectionCard(FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS, true);
assert.calledOnce(spy);
assert.calledWith(spy, SectionsManager.UPDATE_SECTION_CARD, FAKE_ID,
FAKE_URL, FAKE_CARD_OPTIONS, true);
});
it("should do nothing if the section doesn't exist", () => {
SectionsManager.removeSection(FAKE_ID);
const spy = sinon.spy();
SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy);
SectionsManager.updateSectionCard(FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS, true);
assert.notCalled(spy);
});
});
});
describe("SectionsFeed", () => {
@ -204,11 +224,12 @@ describe("SectionsFeed", () => {
it("should bind appropriate listeners", () => {
sinon.spy(SectionsManager, "on");
feed.init();
assert.calledThrice(SectionsManager.on);
assert.callCount(SectionsManager.on, 4);
for (const [event, listener] of [
[SectionsManager.ADD_SECTION, feed.onAddSection],
[SectionsManager.REMOVE_SECTION, feed.onRemoveSection],
[SectionsManager.UPDATE_SECTION, feed.onUpdateSection]
[SectionsManager.UPDATE_SECTION, feed.onUpdateSection],
[SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard]
]) {
assert.calledWith(SectionsManager.on, event, listener);
}
@ -231,11 +252,12 @@ describe("SectionsFeed", () => {
sinon.spy(SectionsManager, "off");
feed.init();
feed.uninit();
assert.calledThrice(SectionsManager.off);
assert.callCount(SectionsManager.off, 4);
for (const [event, listener] of [
[SectionsManager.ADD_SECTION, feed.onAddSection],
[SectionsManager.REMOVE_SECTION, feed.onRemoveSection],
[SectionsManager.UPDATE_SECTION, feed.onUpdateSection]
[SectionsManager.UPDATE_SECTION, feed.onUpdateSection],
[SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard]
]) {
assert.calledWith(SectionsManager.off, event, listener);
}
@ -271,7 +293,7 @@ describe("SectionsFeed", () => {
});
});
describe("#onUpdateSection", () => {
it("should do nothing if no rows are provided", () => {
it("should do nothing if no options are provided", () => {
feed.onUpdateSection(null, FAKE_ID, null);
assert.notCalled(feed.store.dispatch);
});
@ -291,6 +313,27 @@ describe("SectionsFeed", () => {
assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
});
});
describe("#onUpdateSectionCard", () => {
it("should do nothing if no options are provided", () => {
feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, null);
assert.notCalled(feed.store.dispatch);
});
it("should dispatch a SECTION_UPDATE_CARD action with the correct data", () => {
feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS);
const action = feed.store.dispatch.firstCall.args[0];
assert.equal(action.type, "SECTION_UPDATE_CARD");
assert.deepEqual(action.data, {id: FAKE_ID, url: FAKE_URL, options: FAKE_CARD_OPTIONS});
// Should be not broadcast by default, so meta should not exist
assert.notOk(action.meta);
});
it("should broadcast the action only if shouldBroadcast is true", () => {
feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS, true);
const action = feed.store.dispatch.firstCall.args[0];
// Should be broadcast
assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);
assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
});
});
describe("#onAction", () => {
it("should bind this.init to SectionsManager.INIT on INIT", () => {
sinon.spy(SectionsManager, "once");

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

@ -99,10 +99,15 @@ describe("TelemetryFeed", () => {
assert.calledOnce(global.gUUIDGenerator.generateUUID);
assert.equal(session.session_id, global.gUUIDGenerator.generateUUID.firstCall.returnValue);
});
it("should set the page", () => {
it("should set the page if a url parameter is given", () => {
const session = instance.addSession("foo", "about:monkeys");
assert.propertyVal(session, "page", "about:monkeys");
});
it("should set the page prop to 'unknown' if no URL parameter given", () => {
const session = instance.addSession("foo");
assert.equal(session.page, "about:newtab"); // This is hardcoded for now.
assert.propertyVal(session, "page", "unknown");
});
it("should set the perf type when lacking timestamp", () => {
const session = instance.addSession("foo");
@ -178,11 +183,12 @@ describe("TelemetryFeed", () => {
const ping = await instance.createPing();
assert.validate(ping, BasePing);
assert.notProperty(ping, "session_id");
assert.notProperty(ping, "page");
});
it("should create a valid base ping with session info if a portID is supplied", async () => {
// Add a session
const portID = "foo";
instance.addSession(portID);
instance.addSession(portID, "about:home");
const sessionID = instance.sessions.get(portID).session_id;
// Create a ping referencing the session
@ -191,13 +197,13 @@ describe("TelemetryFeed", () => {
// Make sure we added the right session-related stuff to the ping
assert.propertyVal(ping, "session_id", sessionID);
assert.propertyVal(ping, "page", "about:newtab");
assert.propertyVal(ping, "page", "about:home");
});
it("should create an unexpected base ping if no session yet portID is supplied", async () => {
const ping = await instance.createPing("foo");
assert.validate(ping, BasePing);
assert.propertyVal(ping, "page", "about:newtab");
assert.propertyVal(ping, "page", "unknown");
assert.propertyVal(instance.sessions.get("foo").perf, "load_trigger_type", "unexpected");
});
it("should create a base ping with user_prefs", async () => {
@ -490,11 +496,11 @@ describe("TelemetryFeed", () => {
instance.onAction(ac.SendToMain({
type: at.NEW_TAB_INIT,
data: {}
data: {url: "about:monkeys"}
}, "port123"));
assert.calledOnce(stub);
assert.calledWith(stub, "port123");
assert.calledWith(stub, "port123", "about:monkeys");
});
it("should call .endSession() on a NEW_TAB_UNLOAD action", () => {
const stub = sandbox.stub(instance, "endSession");

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

@ -385,6 +385,17 @@ describe("Top Sites Feed", () => {
data: [Object.assign({}, site1, {hostname: "foo.com"})]
}));
});
it("should compare against links if available, instead of getting from store", () => {
const frecentSite = {url: "foo.com", faviconSize: 32, favicon: "favicon.png"};
const pinnedSite1 = {url: "bar.com"};
const pinnedSite2 = {url: "foo.com"};
fakeNewTabUtils.pinnedLinks.links = [pinnedSite1, pinnedSite2];
feed.store = {getState() { return {TopSites: {rows: sinon.spy()}}; }};
let result = feed._getPinnedWithData([frecentSite]);
assert.deepEqual(result[0], pinnedSite1);
assert.deepEqual(result[1], Object.assign({}, frecentSite, pinnedSite2));
assert.notCalled(feed.store.getState().TopSites.rows);
});
it("should call unpin with correct parameters on TOP_SITES_UNPIN", () => {
fakeNewTabUtils.pinnedLinks.links = [null, null, {url: "foo.com"}, null, null, null, null, null, FAKE_LINKS[0]];
const unpinAction = {