Bug 1382785 - Add Pocket, search delay, and bug fixes to Activity Stream r=Mardak
MozReview-Commit-ID: CQEN0Rzy6TX --HG-- extra : rebase_source : 010c160c5689634056ffc81f6efb5d65961e14b8
@ -31,6 +31,7 @@ for (const type of [
|
||||
"DELETE_HISTORY_URL_CONFIRM",
|
||||
"DIALOG_CANCEL",
|
||||
"DIALOG_OPEN",
|
||||
"FEED_INIT",
|
||||
"INIT",
|
||||
"LOCALE_UPDATED",
|
||||
"NEW_TAB_INITIAL_STATE",
|
||||
@ -50,7 +51,13 @@ for (const type of [
|
||||
"PREF_CHANGED",
|
||||
"SAVE_TO_POCKET",
|
||||
"SCREENSHOT_UPDATED",
|
||||
"SECTION_DEREGISTER",
|
||||
"SECTION_REGISTER",
|
||||
"SECTION_ROWS_UPDATE",
|
||||
"SET_PREF",
|
||||
"SNIPPETS_DATA",
|
||||
"SNIPPETS_RESET",
|
||||
"SYSTEM_TICK",
|
||||
"TELEMETRY_PERFORMANCE_EVENT",
|
||||
"TELEMETRY_UNDESIRED_EVENT",
|
||||
"TELEMETRY_USER_EVENT",
|
||||
|
@ -16,6 +16,7 @@ const INITIAL_STATE = {
|
||||
// The version of the system-addon
|
||||
version: null
|
||||
},
|
||||
Snippets: {initialized: false},
|
||||
TopSites: {
|
||||
// Have we received real data from history yet?
|
||||
initialized: false,
|
||||
@ -29,7 +30,8 @@ const INITIAL_STATE = {
|
||||
Dialog: {
|
||||
visible: false,
|
||||
data: {}
|
||||
}
|
||||
},
|
||||
Sections: []
|
||||
};
|
||||
|
||||
function App(prevState = INITIAL_STATE.App, action) {
|
||||
@ -105,6 +107,9 @@ function TopSites(prevState = INITIAL_STATE.TopSites, action) {
|
||||
});
|
||||
return hasMatch ? Object.assign({}, prevState, {rows: newRows}) : prevState;
|
||||
case at.PLACES_BOOKMARK_ADDED:
|
||||
if (!action.data) {
|
||||
return prevState;
|
||||
}
|
||||
newRows = prevState.rows.map(site => {
|
||||
if (site && site.url === action.data.url) {
|
||||
const {bookmarkGuid, bookmarkTitle, lastModified} = action.data;
|
||||
@ -114,6 +119,9 @@ function TopSites(prevState = INITIAL_STATE.TopSites, action) {
|
||||
});
|
||||
return Object.assign({}, prevState, {rows: newRows});
|
||||
case at.PLACES_BOOKMARK_REMOVED:
|
||||
if (!action.data) {
|
||||
return prevState;
|
||||
}
|
||||
newRows = prevState.rows.map(site => {
|
||||
if (site && site.url === action.data.url) {
|
||||
const newSite = Object.assign({}, site);
|
||||
@ -165,8 +173,58 @@ function Prefs(prevState = INITIAL_STATE.Prefs, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function Sections(prevState = INITIAL_STATE.Sections, action) {
|
||||
let hasMatch;
|
||||
let newState;
|
||||
switch (action.type) {
|
||||
case at.SECTION_DEREGISTER:
|
||||
return prevState.filter(section => section.id !== action.data);
|
||||
case at.SECTION_REGISTER:
|
||||
// If section exists in prevState, update it
|
||||
newState = prevState.map(section => {
|
||||
if (section && section.id === action.data.id) {
|
||||
hasMatch = true;
|
||||
return Object.assign({}, section, action.data);
|
||||
}
|
||||
return section;
|
||||
});
|
||||
// If section doesn't exist in prevState, create a new section object and
|
||||
// append it to the sections state
|
||||
if (!hasMatch) {
|
||||
const initialized = action.data.rows && action.data.rows.length > 0;
|
||||
newState.push(Object.assign({title: "", initialized, rows: []}, action.data));
|
||||
}
|
||||
return newState;
|
||||
case at.SECTION_ROWS_UPDATE:
|
||||
return prevState.map(section => {
|
||||
if (section && section.id === action.data.id) {
|
||||
return Object.assign({}, section, action.data);
|
||||
}
|
||||
return section;
|
||||
});
|
||||
case at.PLACES_LINK_DELETED:
|
||||
case at.PLACES_LINK_BLOCKED:
|
||||
return prevState.map(section =>
|
||||
Object.assign({}, section, {rows: section.rows.filter(site => site.url !== action.data.url)}));
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
}
|
||||
|
||||
function Snippets(prevState = INITIAL_STATE.Snippets, action) {
|
||||
switch (action.type) {
|
||||
case at.SNIPPETS_DATA:
|
||||
return Object.assign({}, prevState, {initialized: true}, action.data);
|
||||
case at.SNIPPETS_RESET:
|
||||
return INITIAL_STATE.Snippets;
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
}
|
||||
|
||||
this.INITIAL_STATE = INITIAL_STATE;
|
||||
this.reducers = {TopSites, App, Prefs, Dialog};
|
||||
|
||||
this.reducers = {TopSites, App, Snippets, Prefs, Dialog, Sections};
|
||||
this.insertPinned = insertPinned;
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE", "insertPinned"];
|
||||
|
@ -1,3 +1,4 @@
|
||||
@charset "UTF-8";
|
||||
html {
|
||||
box-sizing: border-box; }
|
||||
|
||||
@ -30,6 +31,8 @@ input {
|
||||
vertical-align: middle; }
|
||||
.icon.icon-spacer {
|
||||
margin-inline-end: 8px; }
|
||||
.icon.icon-small-spacer {
|
||||
margin-inline-end: 6px; }
|
||||
.icon.icon-bookmark {
|
||||
background-image: url("assets/glyph-bookmark-16.svg"); }
|
||||
.icon.icon-bookmark-remove {
|
||||
@ -50,11 +53,19 @@ input {
|
||||
background-image: url("assets/glyph-unpin-16.svg"); }
|
||||
.icon.icon-pocket {
|
||||
background-image: url("assets/glyph-pocket-16.svg"); }
|
||||
.icon.icon-historyItem {
|
||||
background-image: url("assets/glyph-historyItem-16.svg"); }
|
||||
.icon.icon-trending {
|
||||
background-image: url("assets/glyph-trending-16.svg"); }
|
||||
.icon.icon-now {
|
||||
background-image: url("assets/glyph-now-16.svg"); }
|
||||
.icon.icon-pin-small {
|
||||
background-image: url("assets/glyph-pin-12.svg");
|
||||
background-size: 12px;
|
||||
height: 12px;
|
||||
width: 12px; }
|
||||
.icon.icon-check {
|
||||
background-image: url("chrome://browser/skin/check.svg"); }
|
||||
|
||||
html,
|
||||
body,
|
||||
@ -134,6 +145,19 @@ a {
|
||||
color: #FFF;
|
||||
margin-inline-start: auto; }
|
||||
|
||||
#snippets-container {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
height: 122px; }
|
||||
|
||||
#snippets {
|
||||
max-width: 736px;
|
||||
margin: 0 auto; }
|
||||
|
||||
.outer-wrapper {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
@ -149,7 +173,7 @@ main {
|
||||
main {
|
||||
width: 736px; } }
|
||||
main section {
|
||||
margin-bottom: 41px; }
|
||||
margin-bottom: 40px; }
|
||||
|
||||
.section-title {
|
||||
color: #6E707E;
|
||||
@ -205,10 +229,10 @@ main {
|
||||
.top-sites-list .top-site-outer .context-menu-button:focus, .top-sites-list .top-site-outer .context-menu-button:active {
|
||||
transform: scale(1);
|
||||
opacity: 1; }
|
||||
.top-sites-list .top-site-outer:hover .tile, .top-sites-list .top-site-outer:active .tile, .top-sites-list .top-site-outer:focus .tile, .top-sites-list .top-site-outer.active .tile {
|
||||
.top-sites-list .top-site-outer:hover .tile, .top-sites-list .top-site-outer:focus .tile, .top-sites-list .top-site-outer.active .tile {
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 150ms; }
|
||||
.top-sites-list .top-site-outer:hover .context-menu-button, .top-sites-list .top-site-outer:active .context-menu-button, .top-sites-list .top-site-outer:focus .context-menu-button, .top-sites-list .top-site-outer.active .context-menu-button {
|
||||
.top-sites-list .top-site-outer:hover .context-menu-button, .top-sites-list .top-site-outer:focus .context-menu-button, .top-sites-list .top-site-outer.active .context-menu-button {
|
||||
transform: scale(1);
|
||||
opacity: 1; }
|
||||
.top-sites-list .top-site-outer .tile {
|
||||
@ -258,6 +282,117 @@ main {
|
||||
.top-sites-list .top-site-outer .title.pinned span {
|
||||
padding: 0 13px; }
|
||||
|
||||
.sections-list .section-top-bar {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
margin-bottom: 18px; }
|
||||
.sections-list .section-top-bar .section-title {
|
||||
float: left; }
|
||||
.sections-list .section-top-bar .section-info-option {
|
||||
float: right; }
|
||||
.sections-list .section-top-bar .info-option-icon {
|
||||
background-image: url("assets/glyph-info-option-12.svg");
|
||||
background-size: 12px 12px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: inline-block; }
|
||||
.sections-list .section-top-bar .section-info-option div {
|
||||
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 {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: visibility 0.2s, opacity 0.2s ease-out; }
|
||||
.sections-list .section-top-bar .info-option {
|
||||
z-index: 9999;
|
||||
position: absolute;
|
||||
background: #FFF;
|
||||
border: solid 1px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
color: #0C0C0D;
|
||||
line-height: 120%;
|
||||
width: 320px;
|
||||
right: 0;
|
||||
top: 34px;
|
||||
margin-top: -4px;
|
||||
margin-right: -4px;
|
||||
padding: 24px;
|
||||
-moz-user-select: none; }
|
||||
.sections-list .section-top-bar .info-option-header {
|
||||
font-size: 15px;
|
||||
font-weight: 600; }
|
||||
.sections-list .section-top-bar .info-option-body {
|
||||
margin: 0;
|
||||
margin-top: 12px; }
|
||||
.sections-list .section-top-bar .info-option-link {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
color: #0A84FF; }
|
||||
|
||||
.sections-list .section-list {
|
||||
width: 768px;
|
||||
clear: both;
|
||||
margin: 0; }
|
||||
|
||||
.sections-list .section-empty-state {
|
||||
width: 100%;
|
||||
height: 266px;
|
||||
display: flex;
|
||||
border: solid 1px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px; }
|
||||
.sections-list .section-empty-state .empty-state {
|
||||
margin: auto;
|
||||
max-width: 350px; }
|
||||
.sections-list .section-empty-state .empty-state .empty-state-icon {
|
||||
background-size: 50px 50px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
fill: rgba(160, 160, 160, 0.4);
|
||||
-moz-context-properties: fill;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
margin: 0 auto;
|
||||
display: block; }
|
||||
.sections-list .section-empty-state .empty-state .empty-state-message {
|
||||
margin-bottom: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
color: #A0A0A0;
|
||||
text-align: center; }
|
||||
|
||||
.topic {
|
||||
font-size: 13px;
|
||||
color: #BFC0C7;
|
||||
min-width: 780px; }
|
||||
.topic ul {
|
||||
display: inline;
|
||||
padding-left: 12px; }
|
||||
.topic ul li {
|
||||
display: inline; }
|
||||
.topic ul li::after {
|
||||
content: '•';
|
||||
padding-left: 8px;
|
||||
padding-right: 8px; }
|
||||
.topic ul li:last-child::after {
|
||||
content: none; }
|
||||
.topic .topic-link {
|
||||
color: #008EA4; }
|
||||
.topic .topic-read-more {
|
||||
float: right;
|
||||
margin-right: 40px;
|
||||
color: #008EA4; }
|
||||
.topic .topic-read-more-logo {
|
||||
padding-right: 10px;
|
||||
margin-left: 5px;
|
||||
background-image: url("assets/topic-show-more-12.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position-y: 2px; }
|
||||
|
||||
.search-wrapper {
|
||||
cursor: default;
|
||||
display: flex;
|
||||
@ -516,3 +651,109 @@ main {
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
z-index: 11002; }
|
||||
|
||||
.card-outer {
|
||||
background: #FFF;
|
||||
display: inline-block;
|
||||
margin-inline-end: 32px;
|
||||
margin-bottom: 16px;
|
||||
width: 224px;
|
||||
border-radius: 3px;
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
height: 266px;
|
||||
position: relative; }
|
||||
.card-outer .context-menu-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: -13.5px;
|
||||
offset-inline-end: -13.5px;
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
background-color: #FFF;
|
||||
background-image: url("assets/glyph-more-16.svg");
|
||||
background-position: 65%;
|
||||
background-repeat: no-repeat;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.1);
|
||||
transform: scale(0.25);
|
||||
opacity: 0;
|
||||
transition-property: transform, opacity;
|
||||
transition-duration: 200ms;
|
||||
z-index: 399; }
|
||||
.card-outer .context-menu-button:focus, .card-outer .context-menu-button:active {
|
||||
transform: scale(1);
|
||||
opacity: 1; }
|
||||
.card-outer .card {
|
||||
height: 100%;
|
||||
border-radius: 3px; }
|
||||
.card-outer > a {
|
||||
display: block;
|
||||
color: inherit;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
position: absolute; }
|
||||
.card-outer > a.active .card, .card-outer > a:focus .card {
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 150ms; }
|
||||
.card-outer:hover, .card-outer:focus, .card-outer.active {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 150ms; }
|
||||
.card-outer:hover .context-menu-button, .card-outer:focus .context-menu-button, .card-outer.active .context-menu-button {
|
||||
transform: scale(1);
|
||||
opacity: 1; }
|
||||
.card-outer .card-preview-image {
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
height: 122px;
|
||||
border-bottom-color: rgba(0, 0, 0, 0.1);
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-radius: 3px 3px 0 0; }
|
||||
.card-outer .card-details {
|
||||
padding: 10px 16px 12px; }
|
||||
.card-outer .card-text {
|
||||
overflow: hidden;
|
||||
max-height: 78px; }
|
||||
.card-outer .card-text.full-height {
|
||||
max-height: 200px; }
|
||||
.card-outer .card-host-name {
|
||||
color: #858585;
|
||||
font-size: 10px;
|
||||
padding-bottom: 4px;
|
||||
text-transform: uppercase; }
|
||||
.card-outer .card-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: inherit;
|
||||
word-wrap: break-word; }
|
||||
.card-outer .card-description {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
line-height: 18px;
|
||||
max-height: 34px; }
|
||||
.card-outer .card-context {
|
||||
padding: 16px 16px 14px 14px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
color: #A0A0A0;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center; }
|
||||
.card-outer .card-context-icon {
|
||||
opacity: 0.5;
|
||||
font-size: 13px;
|
||||
margin-inline-end: 6px;
|
||||
display: block; }
|
||||
.card-outer .card-context-label {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
|
@ -8,6 +8,10 @@
|
||||
</head>
|
||||
<body class="activity-stream">
|
||||
<div id="root"></div>
|
||||
<div id="snippets-container">
|
||||
<div id="topSection"></div> <!-- TODO: placeholder for v4 snippets. It should be removed when we switch to v5 -->
|
||||
<div id="snippets"></div>
|
||||
</div>
|
||||
<script src="chrome://browser/content/contentSearchUI.js"></script>
|
||||
<script src="resource://activity-stream/vendor/react.js"></script>
|
||||
<script src="resource://activity-stream/vendor/react-dom.js"></script>
|
||||
|
@ -0,0 +1,6 @@
|
||||
<!-- 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/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#4d4d4d" d="M365,190a4,4,0,1,1,4-4A4,4,0,0,1,365,190Zm0-6a2,2,0,1,0,2,2A2,2,0,0,0,365,184Z" transform="translate(-357 -178)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 450 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><path fill="#999" d="M6 0a6 6 0 1 0 6 6 6 6 0 0 0-6-6zm.7 10.26a1.13 1.13 0 0 1-.78.28 1.13 1.13 0 0 1-.78-.28 1 1 0 0 1 0-1.42 1.13 1.13 0 0 1 .78-.28 1.13 1.13 0 0 1 .78.28 1 1 0 0 1 0 1.42zM8.55 5a3 3 0 0 1-.62.81l-.67.63a1.58 1.58 0 0 0-.4.57 2.24 2.24 0 0 0-.12.74H5.06a3.82 3.82 0 0 1 .19-1.35 2.11 2.11 0 0 1 .63-.86 4.17 4.17 0 0 0 .66-.67 1.09 1.09 0 0 0 .23-.67.73.73 0 0 0-.77-.86.71.71 0 0 0-.57.26 1.1 1.1 0 0 0-.23.7h-2A2.36 2.36 0 0 1 4 2.47a2.94 2.94 0 0 1 2-.65 3.06 3.06 0 0 1 2 .6 2.12 2.12 0 0 1 .72 1.72 2 2 0 0 1-.17.86z"/></svg>
|
After Width: | Height: | Size: 612 B |
@ -0,0 +1,6 @@
|
||||
<!-- 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/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#4d4d4d" d="M8 0a8 8 0 1 0 8 8 8.009 8.009 0 0 0-8-8zm0 14a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zm3.5-6H8V4.5a.5.5 0 0 0-1 0v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 0-1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 479 B |
@ -2,5 +2,5 @@
|
||||
- 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/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="context-fill" d="M8 15a8 8 0 0 1-8-8V3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4a8 8 0 0 1-8 8zm3.985-10.032a.99.99 0 0 0-.725.319L7.978 8.57 4.755 5.336A.984.984 0 0 0 4 4.968a1 1 0 0 0-.714 1.7l-.016.011 3.293 3.306.707.707a1 1 0 0 0 1.414 0l.707-.707L12.7 6.679a1 1 0 0 0-.715-1.711z"/>
|
||||
</svg>
|
||||
<path fill="#4d4d4d" d="M8 15a8 8 0 0 1-8-8V3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4a8 8 0 0 1-8 8zm3.985-10.032a.99.99 0 0 0-.725.319L7.978 8.57 4.755 5.336A.984.984 0 0 0 4 4.968a1 1 0 0 0-.714 1.7l-.016.011 3.293 3.306.707.707a1 1 0 0 0 1.414 0l.707-.707L12.7 6.679a1 1 0 0 0-.715-1.711z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 597 B After Width: | Height: | Size: 593 B |
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Context-/-Pocket-Trending" fill="#999999">
|
||||
<path d="M12.164765,5.74981818 C12.4404792,5.74981818 12.5976221,6.06981818 12.4233364,6.28509091 C10.7404792,8.37236364 4.26619353,15.6829091 4.15905067,15.744 C5.70047924,12.3301818 7.1276221,8.976 7.1276221,8.976 L4.3276221,8.976 C4.09905067,8.976 3.9376221,8.74472727 4.02333638,8.52654545 C4.70047924,6.77672727 6.86190781,1.32945455 7.30476495,0.216727273 C7.35333638,0.0916363636 7.46190781,0.0174545455 7.59476495,0.016 C8.32476495,0.0130909091 10.7904792,0.00290909091 12.5790507,0 C12.844765,0 12.9976221,0.305454545 12.8433364,0.525090909 L9.17190781,5.74981818 L12.164765,5.74981818 Z" id="Fill-1"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 985 B |
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Icon / ></title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="Icon-/->" stroke-width="2" stroke="#008EA4">
|
||||
<polyline id="Path-2" points="4 2 8 6 4 10"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 644 B |
@ -1025,6 +1025,7 @@
|
||||
"header_stories": "Top Stories",
|
||||
"header_visit_again": "Visit Again",
|
||||
"header_bookmarks": "Recent Bookmarks",
|
||||
"header_recommended_by": "Recommended by {provider}",
|
||||
"header_bookmarks_placeholder": "You don’t have any bookmarks yet.",
|
||||
"header_stories_from": "from",
|
||||
"type_label_visited": "Visited",
|
||||
@ -1051,6 +1052,7 @@
|
||||
"search_header": "{search_engine_name} Search",
|
||||
"search_web_placeholder": "Search the Web",
|
||||
"search_settings": "Change Search Settings",
|
||||
"section_info_option": "Info",
|
||||
"welcome_title": "Welcome to new tab",
|
||||
"welcome_body": "Firefox will use this space to show your most relevant bookmarks, articles, videos, and pages you’ve recently visited, so you can get back to them easily.",
|
||||
"welcome_label": "Identifying your Highlights",
|
||||
@ -1095,7 +1097,8 @@
|
||||
"pocket_read_even_more": "View More Stories",
|
||||
"pocket_feedback_header": "The best of the web, curated by over 25 million people.",
|
||||
"pocket_feedback_body": "Pocket, a part of the Mozilla family, will help connect you to high-quality content that you may not have found otherwise.",
|
||||
"pocket_send_feedback": "Send Feedback"
|
||||
"pocket_send_feedback": "Send Feedback",
|
||||
"empty_state_topstories": "You’ve caught up. Check back later for more top stories from Pocket. Can’t wait? Select a popular topic to find more great stories from around the web."
|
||||
},
|
||||
"en-ZA": {},
|
||||
"eo": {
|
||||
|
@ -14,11 +14,34 @@ const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm",
|
||||
const {PlacesFeed} = Cu.import("resource://activity-stream/lib/PlacesFeed.jsm", {});
|
||||
const {PrefsFeed} = Cu.import("resource://activity-stream/lib/PrefsFeed.jsm", {});
|
||||
const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
|
||||
const {SnippetsFeed} = Cu.import("resource://activity-stream/lib/SnippetsFeed.jsm", {});
|
||||
const {SystemTickFeed} = Cu.import("resource://activity-stream/lib/SystemTickFeed.jsm", {});
|
||||
const {TelemetryFeed} = Cu.import("resource://activity-stream/lib/TelemetryFeed.jsm", {});
|
||||
const {TopSitesFeed} = Cu.import("resource://activity-stream/lib/TopSitesFeed.jsm", {});
|
||||
const {TopStoriesFeed} = Cu.import("resource://activity-stream/lib/TopStoriesFeed.jsm", {});
|
||||
|
||||
const REASON_ADDON_UNINSTALL = 6;
|
||||
|
||||
// Sections, keyed by section id
|
||||
const SECTIONS = new Map([
|
||||
["topstories", {
|
||||
feed: TopStoriesFeed,
|
||||
prefTitle: "Fetches content recommendations from a configurable content provider",
|
||||
showByDefault: false
|
||||
}]
|
||||
]);
|
||||
|
||||
const SECTION_FEEDS_CONFIG = Array.from(SECTIONS.entries()).map(entry => {
|
||||
const id = entry[0];
|
||||
const {feed: Feed, prefTitle, showByDefault: value} = entry[1];
|
||||
return {
|
||||
name: `section.${id}`,
|
||||
factory: () => new Feed(),
|
||||
title: prefTitle || `${id} section feed`,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
const PREFS_CONFIG = new Map([
|
||||
["default.sites", {
|
||||
title: "Comma-separated list of default top sites to fill in behind visited sites",
|
||||
@ -45,11 +68,24 @@ const PREFS_CONFIG = new Map([
|
||||
["telemetry.ping.endpoint", {
|
||||
title: "Telemetry server endpoint",
|
||||
value: "https://tiles.services.mozilla.com/v4/links/activity-stream"
|
||||
}],
|
||||
["feeds.section.topstories.options", {
|
||||
title: "Configuration options for top stories feed",
|
||||
value: `{
|
||||
"stories_endpoint": "https://getpocket.com/v3/firefox/global-recs?consumer_key=$apiKey",
|
||||
"topics_endpoint": "https://getpocket.com/v3/firefox/trending-topics?consumer_key=$apiKey",
|
||||
"read_more_endpoint": "https://getpocket.com/explore/trending?src=ff_new_tab",
|
||||
"learn_more_endpoint": "https://getpocket.com/firefox_learnmore?src=ff_newtab",
|
||||
"survey_link": "https://www.surveymonkey.com/r/newtabffx",
|
||||
"api_key_pref": "extensions.pocket.oAuthConsumerKey",
|
||||
"provider_name": "Pocket",
|
||||
"provider_icon": "pocket"
|
||||
}`
|
||||
}]
|
||||
]);
|
||||
|
||||
const FEEDS_CONFIG = new Map();
|
||||
for (const {name, factory, title, value} of [
|
||||
for (const {name, factory, title, value} of SECTION_FEEDS_CONFIG.concat([
|
||||
{
|
||||
name: "localization",
|
||||
factory: () => new LocalizationFeed(),
|
||||
@ -74,6 +110,18 @@ for (const {name, factory, title, value} of [
|
||||
title: "Preferences",
|
||||
value: true
|
||||
},
|
||||
{
|
||||
name: "snippets",
|
||||
factory: () => new SnippetsFeed(),
|
||||
title: "Gets snippets data",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "systemtick",
|
||||
factory: () => new SystemTickFeed(),
|
||||
title: "Produces system tick events to periodically check for data expiry",
|
||||
value: true
|
||||
},
|
||||
{
|
||||
name: "telemetry",
|
||||
factory: () => new TelemetryFeed(),
|
||||
@ -86,7 +134,7 @@ for (const {name, factory, title, value} of [
|
||||
title: "Queries places and gets metadata for Top Sites section",
|
||||
value: true
|
||||
}
|
||||
]) {
|
||||
])) {
|
||||
const pref = `feeds.${name}`;
|
||||
FEEDS_CONFIG.set(pref, factory);
|
||||
PREFS_CONFIG.set(pref, {title, value});
|
||||
@ -135,4 +183,4 @@ this.ActivityStream = class ActivityStream {
|
||||
};
|
||||
|
||||
this.PREFS_CONFIG = PREFS_CONFIG;
|
||||
this.EXPORTED_SYMBOLS = ["ActivityStream"];
|
||||
this.EXPORTED_SYMBOLS = ["ActivityStream", "SECTIONS"];
|
||||
|
@ -13,6 +13,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
|
||||
"resource://gre/modules/NewTabUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Pocket",
|
||||
"chrome://pocket/content/Pocket.jsm");
|
||||
|
||||
const LINK_BLOCKED_EVENT = "newtab-linkBlocked";
|
||||
|
||||
@ -205,6 +207,9 @@ class PlacesFeed {
|
||||
case at.DELETE_HISTORY_URL:
|
||||
NewTabUtils.activityStreamLinks.deleteHistoryEntry(action.data);
|
||||
break;
|
||||
case at.SAVE_TO_POCKET:
|
||||
Pocket.savePage(action._target.browser, action.data.site.url, action.data.site.title);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
58
browser/extensions/activity-stream/lib/SnippetsFeed.jsm
Normal file
@ -0,0 +1,58 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Console.jsm");
|
||||
const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
// Url to fetch snippets, in the urlFormatter service format.
|
||||
const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
|
||||
|
||||
// Should be bumped up if the snippets content format changes.
|
||||
const STARTPAGE_VERSION = 4;
|
||||
|
||||
this.SnippetsFeed = class SnippetsFeed {
|
||||
constructor() {
|
||||
this._onUrlChange = this._onUrlChange.bind(this);
|
||||
}
|
||||
get snippetsURL() {
|
||||
const updateURL = Services
|
||||
.prefs.getStringPref(SNIPPETS_URL_PREF)
|
||||
.replace("%STARTPAGE_VERSION%", STARTPAGE_VERSION);
|
||||
return Services.urlFormatter.formatURL(updateURL);
|
||||
}
|
||||
init() {
|
||||
const data = {
|
||||
snippetsURL: this.snippetsURL,
|
||||
version: STARTPAGE_VERSION
|
||||
};
|
||||
this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_DATA, data}));
|
||||
Services.prefs.addObserver(SNIPPETS_URL_PREF, this._onUrlChange);
|
||||
}
|
||||
uninit() {
|
||||
this.store.dispatch({type: at.SNIPPETS_RESET});
|
||||
Services.prefs.removeObserver(SNIPPETS_URL_PREF, this._onUrlChange);
|
||||
}
|
||||
_onUrlChange() {
|
||||
this.store.dispatch(ac.BroadcastToContent({
|
||||
type: at.SNIPPETS_DATA,
|
||||
data: {snippetsURL: this.snippetsURL}
|
||||
}));
|
||||
}
|
||||
onAction(action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
this.init();
|
||||
break;
|
||||
case at.FEED_INIT:
|
||||
if (action.data === "feeds.snippets") { this.init(); }
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["SnippetsFeed"];
|
@ -9,6 +9,7 @@ const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib
|
||||
const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
|
||||
const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
|
||||
const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
|
||||
const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
/**
|
||||
* Store - This has a similar structure to a redux store, but includes some extra
|
||||
@ -91,6 +92,7 @@ this.Store = class Store {
|
||||
if (this._feedFactories.has(name)) {
|
||||
if (value) {
|
||||
this.initFeed(name);
|
||||
this.dispatch({type: at.FEED_INIT, data: name});
|
||||
} else {
|
||||
this.uninitFeed(name);
|
||||
}
|
||||
|
35
browser/extensions/activity-stream/lib/SystemTickFeed.jsm
Normal file
@ -0,0 +1,35 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "setInterval", "resource://gre/modules/Timer.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "clearInterval", "resource://gre/modules/Timer.jsm");
|
||||
|
||||
// Frequency at which SYSTEM_TICK events are fired
|
||||
const SYSTEM_TICK_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
this.SystemTickFeed = class SystemTickFeed {
|
||||
init() {
|
||||
this.intervalId = setInterval(() => this.store.dispatch({type: at.SYSTEM_TICK}), SYSTEM_TICK_INTERVAL);
|
||||
}
|
||||
|
||||
onAction(action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
this.init();
|
||||
break;
|
||||
case at.UNINIT:
|
||||
clearInterval(this.intervalId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.SYSTEM_TICK_INTERVAL = SYSTEM_TICK_INTERVAL;
|
||||
this.EXPORTED_SYMBOLS = ["SystemTickFeed", "SYSTEM_TICK_INTERVAL"];
|
187
browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
Normal file
@ -0,0 +1,187 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/NewTabUtils.jsm");
|
||||
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 STORIES_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
|
||||
const TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours
|
||||
const SECTION_ID = "TopStories";
|
||||
|
||||
this.TopStoriesFeed = class TopStoriesFeed {
|
||||
constructor() {
|
||||
this.storiesLastUpdated = 0;
|
||||
this.topicsLastUpdated = 0;
|
||||
}
|
||||
|
||||
init() {
|
||||
try {
|
||||
const prefs = new Prefs();
|
||||
const options = JSON.parse(prefs.get("feeds.section.topstories.options"));
|
||||
const apiKey = this._getApiKeyFromPref(options.api_key_pref);
|
||||
this.stories_endpoint = this._produceUrlWithApiKey(options.stories_endpoint, apiKey);
|
||||
this.topics_endpoint = this._produceUrlWithApiKey(options.topics_endpoint, apiKey);
|
||||
this.read_more_endpoint = options.read_more_endpoint;
|
||||
|
||||
// TODO https://github.com/mozilla/activity-stream/issues/2902
|
||||
const sectionOptions = {
|
||||
id: SECTION_ID,
|
||||
icon: options.provider_icon,
|
||||
title: {id: "header_recommended_by", values: {provider: options.provider_name}},
|
||||
rows: [],
|
||||
maxCards: 3,
|
||||
contextMenuOptions: ["SaveToPocket", "Separator", "CheckBookmark", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
|
||||
infoOption: {
|
||||
header: {id: "pocket_feedback_header"},
|
||||
body: {id: "pocket_feedback_body"},
|
||||
link: {
|
||||
href: options.survey_link,
|
||||
id: "pocket_send_feedback"
|
||||
}
|
||||
},
|
||||
emptyState: {
|
||||
message: {id: "empty_state_topstories"},
|
||||
icon: "check"
|
||||
}
|
||||
};
|
||||
this.store.dispatch(ac.BroadcastToContent({type: at.SECTION_REGISTER, data: sectionOptions}));
|
||||
|
||||
this.fetchStories();
|
||||
this.fetchTopics();
|
||||
} catch (e) {
|
||||
Cu.reportError(`Problem initializing top stories feed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
uninit() {
|
||||
this.store.dispatch(ac.BroadcastToContent({type: at.SECTION_DEREGISTER, data: SECTION_ID}));
|
||||
}
|
||||
|
||||
async fetchStories() {
|
||||
if (this.stories_endpoint) {
|
||||
const stories = await fetch(this.stories_endpoint)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.text();
|
||||
}
|
||||
throw new Error(`Stories endpoint returned unexpected status: ${response.status}`);
|
||||
})
|
||||
.then(body => {
|
||||
let items = JSON.parse(body).list;
|
||||
items = items
|
||||
.filter(s => !NewTabUtils.blockedLinks.isBlocked(s.dedupe_url))
|
||||
.map(s => ({
|
||||
"guid": s.id,
|
||||
"type": "trending",
|
||||
"title": s.title,
|
||||
"description": s.excerpt,
|
||||
"image": this._normalizeUrl(s.image_src),
|
||||
"url": s.dedupe_url,
|
||||
"lastVisitDate": s.published_timestamp
|
||||
}));
|
||||
return items;
|
||||
})
|
||||
.catch(error => Cu.reportError(`Failed to fetch content: ${error.message}`));
|
||||
|
||||
if (stories) {
|
||||
this.dispatchUpdateEvent(this.storiesLastUpdated,
|
||||
{"type": at.SECTION_ROWS_UPDATE, "data": {"id": SECTION_ID, "rows": stories}});
|
||||
this.storiesLastUpdated = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchTopics() {
|
||||
if (this.topics_endpoint) {
|
||||
const topics = await fetch(this.topics_endpoint)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.text();
|
||||
}
|
||||
throw new Error(`Topics endpoint returned unexpected status: ${response.status}`);
|
||||
})
|
||||
.then(body => JSON.parse(body).topics)
|
||||
.catch(error => Cu.reportError(`Failed to fetch topics: ${error.message}`));
|
||||
|
||||
if (topics) {
|
||||
this.dispatchUpdateEvent(this.topicsLastUpdated,
|
||||
{"type": at.SECTION_ROWS_UPDATE, "data": {"id": SECTION_ID, "topics": topics, "read_more_endpoint": this.read_more_endpoint}});
|
||||
this.topicsLastUpdated = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchUpdateEvent(lastUpdated, evt) {
|
||||
if (lastUpdated === 0) {
|
||||
this.store.dispatch(ac.BroadcastToContent(evt));
|
||||
} else {
|
||||
this.store.dispatch(evt);
|
||||
}
|
||||
}
|
||||
|
||||
_getApiKeyFromPref(apiKeyPref) {
|
||||
if (!apiKeyPref) {
|
||||
return apiKeyPref;
|
||||
}
|
||||
|
||||
return new Prefs().get(apiKeyPref) || Services.prefs.getCharPref(apiKeyPref);
|
||||
}
|
||||
|
||||
_produceUrlWithApiKey(url, apiKey) {
|
||||
if (!url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (url.includes("$apiKey") && !apiKey) {
|
||||
throw new Error(`An API key was specified but none configured: ${url}`);
|
||||
}
|
||||
|
||||
return url.replace("$apiKey", apiKey);
|
||||
}
|
||||
|
||||
// Need to remove parenthesis from image URLs as React will otherwise
|
||||
// fail to render them properly as part of the card template.
|
||||
_normalizeUrl(url) {
|
||||
if (url) {
|
||||
return url.replace(/\(/g, "%28").replace(/\)/g, "%29");
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
onAction(action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
this.init();
|
||||
break;
|
||||
case at.SYSTEM_TICK:
|
||||
if (Date.now() - this.storiesLastUpdated >= STORIES_UPDATE_TIME) {
|
||||
this.fetchStories();
|
||||
}
|
||||
if (Date.now() - this.topicsLastUpdated >= TOPICS_UPDATE_TIME) {
|
||||
this.fetchTopics();
|
||||
}
|
||||
break;
|
||||
case at.UNINIT:
|
||||
this.uninit();
|
||||
break;
|
||||
case at.FEED_INIT:
|
||||
if (action.data === "feeds.section.topstories") {
|
||||
this.init();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.STORIES_UPDATE_TIME = STORIES_UPDATE_TIME;
|
||||
this.TOPICS_UPDATE_TIME = TOPICS_UPDATE_TIME;
|
||||
this.SECTION_ID = SECTION_ID;
|
||||
this.EXPORTED_SYMBOLS = ["TopStoriesFeed", "STORIES_UPDATE_TIME", "TOPICS_UPDATE_TIME", "SECTION_ID"];
|
@ -1,5 +1,4 @@
|
||||
[DEFAULT]
|
||||
skip-if=!nightly_build
|
||||
support-files =
|
||||
blue_page.html
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"activity_stream": true
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
const {reducers, INITIAL_STATE, insertPinned} = require("common/Reducers.jsm");
|
||||
const {TopSites, App, Prefs, Dialog} = reducers;
|
||||
const {TopSites, App, Snippets, Prefs, Dialog, Sections} = reducers;
|
||||
|
||||
const {actionTypes: at} = require("common/Actions.jsm");
|
||||
|
||||
describe("Reducers", () => {
|
||||
@ -77,6 +78,10 @@ describe("Reducers", () => {
|
||||
// old row is unchanged
|
||||
assert.equal(nextState.rows[0], oldState.rows[0]);
|
||||
});
|
||||
it("should not update state for empty action.data on PLACES_BOOKMARK_ADDED", () => {
|
||||
const nextState = TopSites(undefined, {type: at.PLACES_BOOKMARK_ADDED});
|
||||
assert.equal(nextState, INITIAL_STATE.TopSites);
|
||||
});
|
||||
it("should remove a bookmark on PLACES_BOOKMARK_REMOVED", () => {
|
||||
const oldState = {
|
||||
rows: [{url: "foo.com"}, {
|
||||
@ -98,6 +103,10 @@ describe("Reducers", () => {
|
||||
// old row is unchanged
|
||||
assert.deepEqual(nextState.rows[0], oldState.rows[0]);
|
||||
});
|
||||
it("should not update state for empty action.data on PLACES_BOOKMARK_REMOVED", () => {
|
||||
const nextState = TopSites(undefined, {type: at.PLACES_BOOKMARK_REMOVED});
|
||||
assert.equal(nextState, INITIAL_STATE.TopSites);
|
||||
});
|
||||
it("should remove a link on PLACES_LINK_BLOCKED and PLACES_LINK_DELETED", () => {
|
||||
const events = [at.PLACES_LINK_BLOCKED, at.PLACES_LINK_DELETED];
|
||||
events.forEach(event => {
|
||||
@ -179,6 +188,70 @@ describe("Reducers", () => {
|
||||
assert.deepEqual(INITIAL_STATE.Dialog, nextState);
|
||||
});
|
||||
});
|
||||
describe("Sections", () => {
|
||||
let oldState;
|
||||
|
||||
beforeEach(() => {
|
||||
oldState = new Array(5).fill(null).map((v, i) => ({
|
||||
id: `foo_bar_${i}`,
|
||||
title: `Foo Bar ${i}`,
|
||||
initialized: false,
|
||||
rows: [{url: "www.foo.bar"}, {url: "www.other.url"}]
|
||||
}));
|
||||
});
|
||||
|
||||
it("should return INITIAL_STATE by default", () => {
|
||||
assert.equal(INITIAL_STATE.Sections, Sections(undefined, {type: "non_existent"}));
|
||||
});
|
||||
it("should remove the correct section on SECTION_DEREGISTER", () => {
|
||||
const newState = Sections(oldState, {type: at.SECTION_DEREGISTER, data: "foo_bar_2"});
|
||||
assert.lengthOf(newState, 4);
|
||||
const expectedNewState = oldState.splice(2, 1) && oldState;
|
||||
assert.deepEqual(newState, expectedNewState);
|
||||
});
|
||||
it("should add a section on SECTION_REGISTER if it doesn't already exist", () => {
|
||||
const action = {type: at.SECTION_REGISTER, data: {id: "foo_bar_5", title: "Foo Bar 5"}};
|
||||
const newState = Sections(oldState, action);
|
||||
assert.lengthOf(newState, 6);
|
||||
const insertedSection = newState.find(section => section.id === "foo_bar_5");
|
||||
assert.propertyVal(insertedSection, "title", action.data.title);
|
||||
});
|
||||
it("should set newSection.rows === [] if no rows are provided on SECTION_REGISTER", () => {
|
||||
const action = {type: at.SECTION_REGISTER, data: {id: "foo_bar_5", title: "Foo Bar 5"}};
|
||||
const newState = Sections(oldState, action);
|
||||
const insertedSection = newState.find(section => section.id === "foo_bar_5");
|
||||
assert.deepEqual(insertedSection.rows, []);
|
||||
});
|
||||
it("should update a section on SECTION_REGISTER if it already exists", () => {
|
||||
const NEW_TITLE = "New Title";
|
||||
const action = {type: at.SECTION_REGISTER, data: {id: "foo_bar_2", title: NEW_TITLE}};
|
||||
const newState = Sections(oldState, action);
|
||||
assert.lengthOf(newState, 5);
|
||||
const updatedSection = newState.find(section => section.id === "foo_bar_2");
|
||||
assert.ok(updatedSection && updatedSection.title === NEW_TITLE);
|
||||
});
|
||||
it("should have no effect on SECTION_ROWS_UPDATE if the id doesn't exist", () => {
|
||||
const action = {type: at.SECTION_ROWS_UPDATE, data: {id: "fake_id", data: "fake_data"}};
|
||||
const newState = Sections(oldState, action);
|
||||
assert.deepEqual(oldState, newState);
|
||||
});
|
||||
it("should update the section rows with the correct data on SECTION_ROWS_UPDATE", () => {
|
||||
const FAKE_DATA = ["some", "fake", "data"];
|
||||
const action = {type: at.SECTION_ROWS_UPDATE, data: {id: "foo_bar_2", rows: FAKE_DATA}};
|
||||
const newState = Sections(oldState, action);
|
||||
const updatedSection = newState.find(section => section.id === "foo_bar_2");
|
||||
assert.equal(updatedSection.rows, FAKE_DATA);
|
||||
});
|
||||
it("should remove blocked and deleted urls from all rows in all sections", () => {
|
||||
const blockAction = {type: at.PLACES_LINK_BLOCKED, data: {url: "www.foo.bar"}};
|
||||
const deleteAction = {type: at.PLACES_LINK_DELETED, data: {url: "www.foo.bar"}};
|
||||
const newBlockState = Sections(oldState, blockAction);
|
||||
const newDeleteState = Sections(oldState, deleteAction);
|
||||
newBlockState.concat(newDeleteState).forEach(section => {
|
||||
assert.deepEqual(section.rows, [{url: "www.other.url"}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("#insertPinned", () => {
|
||||
let links;
|
||||
|
||||
@ -244,4 +317,23 @@ describe("Reducers", () => {
|
||||
assert.equal(links.length, result.length);
|
||||
});
|
||||
});
|
||||
describe("Snippets", () => {
|
||||
it("should return INITIAL_STATE by default", () => {
|
||||
assert.equal(Snippets(undefined, {type: "some_action"}), INITIAL_STATE.Snippets);
|
||||
});
|
||||
it("should set initialized to true on a SNIPPETS_DATA action", () => {
|
||||
const state = Snippets(undefined, {type: at.SNIPPETS_DATA, data: {}});
|
||||
assert.isTrue(state.initialized);
|
||||
});
|
||||
it("should set the snippet data on a SNIPPETS_DATA action", () => {
|
||||
const data = {snippetsURL: "foo.com", version: 4};
|
||||
const state = Snippets(undefined, {type: at.SNIPPETS_DATA, data});
|
||||
assert.propertyVal(state, "snippetsURL", data.snippetsURL);
|
||||
assert.propertyVal(state, "version", data.version);
|
||||
});
|
||||
it("should reset to the initial state on a SNIPPETS_RESET action", () => {
|
||||
const state = Snippets({initalized: true, foo: "bar"}, {type: at.SNIPPETS_RESET});
|
||||
assert.equal(state, INITIAL_STATE.Snippets);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,17 +6,21 @@ describe("ActivityStream", () => {
|
||||
let sandbox;
|
||||
let as;
|
||||
let ActivityStream;
|
||||
let SECTIONS;
|
||||
function Fake() {}
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
({ActivityStream} = injector({
|
||||
({ActivityStream, SECTIONS} = injector({
|
||||
"lib/LocalizationFeed.jsm": {LocalizationFeed: Fake},
|
||||
"lib/NewTabInit.jsm": {NewTabInit: Fake},
|
||||
"lib/PlacesFeed.jsm": {PlacesFeed: Fake},
|
||||
"lib/TelemetryFeed.jsm": {TelemetryFeed: Fake},
|
||||
"lib/TopSitesFeed.jsm": {TopSitesFeed: Fake},
|
||||
"lib/PrefsFeed.jsm": {PrefsFeed: Fake}
|
||||
"lib/PrefsFeed.jsm": {PrefsFeed: Fake},
|
||||
"lib/SnippetsFeed.jsm": {SnippetsFeed: Fake},
|
||||
"lib/TopStoriesFeed.jsm": {TopStoriesFeed: Fake},
|
||||
"lib/SystemTickFeed.jsm": {SystemTickFeed: Fake}
|
||||
}));
|
||||
as = new ActivityStream();
|
||||
sandbox.stub(as.store, "init");
|
||||
@ -106,5 +110,21 @@ describe("ActivityStream", () => {
|
||||
const feed = as.feeds.get("feeds.prefs")();
|
||||
assert.instanceOf(feed, Fake);
|
||||
});
|
||||
it("should create a section feed for each section in SECTIONS", () => {
|
||||
// If new sections are added, their feeds will have to be added to the
|
||||
// list of injected feeds above for this test to pass
|
||||
SECTIONS.forEach((value, key) => {
|
||||
const feed = as.feeds.get(`feeds.section.${key}`)();
|
||||
assert.instanceOf(feed, Fake);
|
||||
});
|
||||
});
|
||||
it("should create a Snippets feed", () => {
|
||||
const feed = as.feeds.get("feeds.snippets")();
|
||||
assert.instanceOf(feed, Fake);
|
||||
});
|
||||
it("should create a SystemTick feed", () => {
|
||||
const feed = as.feeds.get("feeds.systemtick")();
|
||||
assert.instanceOf(feed, Fake);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -28,6 +28,7 @@ describe("PlacesFeed", () => {
|
||||
history: {addObserver: sandbox.spy(), removeObserver: sandbox.spy()},
|
||||
bookmarks: {TYPE_BOOKMARK, addObserver: sandbox.spy(), removeObserver: sandbox.spy()}
|
||||
});
|
||||
globals.set("Pocket", {savePage: sandbox.spy()});
|
||||
global.Components.classes["@mozilla.org/browser/nav-history-service;1"] = {
|
||||
getService() {
|
||||
return global.PlacesUtils.history;
|
||||
@ -98,6 +99,10 @@ describe("PlacesFeed", () => {
|
||||
feed.onAction({type: at.DELETE_HISTORY_URL, data: "guava.com"});
|
||||
assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteHistoryEntry, "guava.com");
|
||||
});
|
||||
it("should save to Pocket on SAVE_TO_POCKET", () => {
|
||||
feed.onAction({type: at.SAVE_TO_POCKET, data: {site: {url: "raspberry.com", title: "raspberry"}}, _target: {browser: {}}});
|
||||
assert.calledWith(global.Pocket.savePage, {}, "raspberry.com", "raspberry");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#observe", () => {
|
||||
|
@ -0,0 +1,60 @@
|
||||
const {SnippetsFeed} = require("lib/SnippetsFeed.jsm");
|
||||
const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
|
||||
|
||||
describe("SnippetsFeed", () => {
|
||||
let sandbox;
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
it("should dispatch the right version and snippetsURL on INIT", () => {
|
||||
const url = "foo.com/%STARTPAGE_VERSION%";
|
||||
sandbox.stub(global.Services.prefs, "getStringPref").returns(url);
|
||||
const feed = new SnippetsFeed();
|
||||
feed.store = {dispatch: sandbox.stub()};
|
||||
|
||||
feed.onAction({type: at.INIT});
|
||||
|
||||
assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({
|
||||
type: at.SNIPPETS_DATA,
|
||||
data: {
|
||||
snippetsURL: "foo.com/4",
|
||||
version: 4
|
||||
}
|
||||
}));
|
||||
});
|
||||
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.onAction({type: at.FEED_INIT, data: "feeds.snippets"});
|
||||
|
||||
assert.calledOnce(feed.init);
|
||||
});
|
||||
it("should dispatch a SNIPPETS_RESET on uninit", () => {
|
||||
const feed = new SnippetsFeed();
|
||||
feed.store = {dispatch: sandbox.stub()};
|
||||
|
||||
feed.uninit();
|
||||
|
||||
assert.calledWith(feed.store.dispatch, {type: at.SNIPPETS_RESET});
|
||||
});
|
||||
describe("_onUrlChange", () => {
|
||||
it("should dispatch a new snippetsURL", () => {
|
||||
const url = "boo.com/%STARTPAGE_VERSION%";
|
||||
sandbox.stub(global.Services.prefs, "getStringPref").returns(url);
|
||||
const feed = new SnippetsFeed();
|
||||
feed.store = {dispatch: sandbox.stub()};
|
||||
|
||||
feed._onUrlChange();
|
||||
|
||||
assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({
|
||||
type: at.SNIPPETS_DATA,
|
||||
data: {snippetsURL: "boo.com/4"}
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
const injector = require("inject!lib/SystemTickFeed.jsm");
|
||||
const {actionTypes: at} = require("common/Actions.jsm");
|
||||
|
||||
describe("System Tick Feed", () => {
|
||||
let SystemTickFeed;
|
||||
let SYSTEM_TICK_INTERVAL;
|
||||
let instance;
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers();
|
||||
|
||||
({SystemTickFeed, SYSTEM_TICK_INTERVAL} = injector({}));
|
||||
instance = new SystemTickFeed();
|
||||
instance.store = {getState() { return {}; }, dispatch() {}};
|
||||
});
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
it("should create a SystemTickFeed", () => {
|
||||
assert.instanceOf(instance, SystemTickFeed);
|
||||
});
|
||||
it("should fire SYSTEM_TICK events at configured interval", () => {
|
||||
let expectation = sinon.mock(instance.store).expects("dispatch")
|
||||
.twice()
|
||||
.withExactArgs({type: at.SYSTEM_TICK});
|
||||
|
||||
instance.onAction({type: at.INIT});
|
||||
clock.tick(SYSTEM_TICK_INTERVAL * 2);
|
||||
expectation.verify();
|
||||
});
|
||||
it("should not fire SYSTEM_TICK events after UNINIT", () => {
|
||||
let expectation = sinon.mock(instance.store).expects("dispatch")
|
||||
.never();
|
||||
|
||||
instance.onAction({type: at.UNINIT});
|
||||
clock.tick(SYSTEM_TICK_INTERVAL * 2);
|
||||
expectation.verify();
|
||||
});
|
||||
});
|
@ -0,0 +1,257 @@
|
||||
"use strict";
|
||||
const injector = require("inject!lib/TopStoriesFeed.jsm");
|
||||
const {FakePrefs} = require("test/unit/utils");
|
||||
const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
|
||||
const {GlobalOverrider} = require("test/unit/utils");
|
||||
|
||||
describe("Top Stories Feed", () => {
|
||||
let TopStoriesFeed;
|
||||
let STORIES_UPDATE_TIME;
|
||||
let TOPICS_UPDATE_TIME;
|
||||
let SECTION_ID;
|
||||
let instance;
|
||||
let clock;
|
||||
let globals;
|
||||
|
||||
beforeEach(() => {
|
||||
FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
|
||||
"stories_endpoint": "https://somedomain.org/stories?key=$apiKey",
|
||||
"topics_endpoint": "https://somedomain.org/topics?key=$apiKey",
|
||||
"survey_link": "https://www.surveymonkey.com/r/newtabffx",
|
||||
"api_key_pref": "apiKeyPref",
|
||||
"provider_name": "test-provider",
|
||||
"provider_icon": "provider-icon"
|
||||
}`;
|
||||
FakePrefs.prototype.prefs.apiKeyPref = "test-api-key";
|
||||
|
||||
globals = new GlobalOverrider();
|
||||
clock = sinon.useFakeTimers();
|
||||
|
||||
({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}}));
|
||||
instance = new TopStoriesFeed();
|
||||
instance.store = {getState() { return {}; }, dispatch: sinon.spy()};
|
||||
});
|
||||
afterEach(() => {
|
||||
globals.restore();
|
||||
clock.restore();
|
||||
});
|
||||
describe("#init", () => {
|
||||
it("should create a TopStoriesFeed", () => {
|
||||
assert.instanceOf(instance, TopStoriesFeed);
|
||||
});
|
||||
it("should initialize endpoints based on prefs", () => {
|
||||
instance.onAction({type: at.INIT});
|
||||
assert.equal("https://somedomain.org/stories?key=test-api-key", instance.stories_endpoint);
|
||||
assert.equal("https://somedomain.org/topics?key=test-api-key", instance.topics_endpoint);
|
||||
});
|
||||
it("should register section", () => {
|
||||
const expectedSectionOptions = {
|
||||
id: SECTION_ID,
|
||||
icon: "provider-icon",
|
||||
title: {id: "header_recommended_by", values: {provider: "test-provider"}},
|
||||
rows: [],
|
||||
maxCards: 3,
|
||||
contextMenuOptions: ["SaveToPocket", "Separator", "CheckBookmark", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
|
||||
infoOption: {
|
||||
header: {id: "pocket_feedback_header"},
|
||||
body: {id: "pocket_feedback_body"},
|
||||
link: {
|
||||
href: "https://www.surveymonkey.com/r/newtabffx",
|
||||
id: "pocket_send_feedback"
|
||||
}
|
||||
},
|
||||
emptyState: {
|
||||
message: {id: "empty_state_topstories"},
|
||||
icon: "check"
|
||||
}
|
||||
};
|
||||
|
||||
instance.onAction({type: at.INIT});
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_REGISTER);
|
||||
assert.calledWith(instance.store.dispatch, ac.BroadcastToContent({
|
||||
type: at.SECTION_REGISTER,
|
||||
data: expectedSectionOptions
|
||||
}));
|
||||
});
|
||||
it("should fetch stories on init", () => {
|
||||
instance.fetchStories = sinon.spy();
|
||||
instance.fetchTopics = sinon.spy();
|
||||
instance.onAction({type: at.INIT});
|
||||
assert.calledOnce(instance.fetchStories);
|
||||
});
|
||||
it("should fetch topics on init", () => {
|
||||
instance.fetchStories = sinon.spy();
|
||||
instance.fetchTopics = sinon.spy();
|
||||
instance.onAction({type: at.INIT});
|
||||
assert.calledOnce(instance.fetchTopics);
|
||||
});
|
||||
it("should not fetch if endpoint not configured", () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
FakePrefs.prototype.prefs["feeds.section.topstories.options"] = "{}";
|
||||
instance.init();
|
||||
assert.notCalled(fetchStub);
|
||||
});
|
||||
it("should report error for invalid configuration", () => {
|
||||
globals.sandbox.spy(global.Components.utils, "reportError");
|
||||
FakePrefs.prototype.prefs["feeds.section.topstories.options"] = "invalid";
|
||||
instance.init();
|
||||
|
||||
assert.called(Components.utils.reportError);
|
||||
});
|
||||
it("should report error for missing api key", () => {
|
||||
let fakeServices = {prefs: {getCharPref: sinon.spy()}};
|
||||
globals.set("Services", fakeServices);
|
||||
globals.sandbox.spy(global.Components.utils, "reportError");
|
||||
FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
|
||||
"stories_endpoint": "https://somedomain.org/stories?key=$apiKey",
|
||||
"topics_endpoint": "https://somedomain.org/topics?key=$apiKey"
|
||||
}`;
|
||||
instance.init();
|
||||
|
||||
assert.called(Components.utils.reportError);
|
||||
});
|
||||
it("should deregister section", () => {
|
||||
instance.onAction({type: at.UNINIT});
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
assert.calledWith(instance.store.dispatch, ac.BroadcastToContent({
|
||||
type: at.SECTION_DEREGISTER,
|
||||
data: SECTION_ID
|
||||
}));
|
||||
});
|
||||
it("should initialize on FEED_INIT", () => {
|
||||
instance.init = sinon.spy();
|
||||
instance.onAction({type: at.FEED_INIT, data: "feeds.section.topstories"});
|
||||
assert.calledOnce(instance.init);
|
||||
});
|
||||
});
|
||||
describe("#fetch", () => {
|
||||
it("should fetch stories and send event", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
|
||||
|
||||
const response = `{"list": [{"id" : "1",
|
||||
"title": "title",
|
||||
"excerpt": "description",
|
||||
"image_src": "image-url",
|
||||
"dedupe_url": "rec-url",
|
||||
"published_timestamp" : "123"
|
||||
}]}`;
|
||||
const stories = [{
|
||||
"guid": "1",
|
||||
"type": "trending",
|
||||
"title": "title",
|
||||
"description": "description",
|
||||
"image": "image-url",
|
||||
"url": "rec-url",
|
||||
"lastVisitDate": "123"
|
||||
}];
|
||||
|
||||
instance.stories_endpoint = "stories-endpoint";
|
||||
fetchStub.resolves({ok: true, status: 200, text: () => response});
|
||||
await instance.fetchStories();
|
||||
|
||||
assert.calledOnce(fetchStub);
|
||||
assert.calledWithExactly(fetchStub, instance.stories_endpoint);
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_ROWS_UPDATE);
|
||||
assert.deepEqual(instance.store.dispatch.firstCall.args[0].data.id, SECTION_ID);
|
||||
assert.deepEqual(instance.store.dispatch.firstCall.args[0].data.rows, stories);
|
||||
});
|
||||
it("should dispatch events", () => {
|
||||
instance.dispatchUpdateEvent(123, {});
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
});
|
||||
it("should report error for unexpected stories response", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
globals.sandbox.spy(global.Components.utils, "reportError");
|
||||
|
||||
instance.stories_endpoint = "stories-endpoint";
|
||||
fetchStub.resolves({ok: false, status: 400});
|
||||
await instance.fetchStories();
|
||||
|
||||
assert.calledOnce(fetchStub);
|
||||
assert.calledWithExactly(fetchStub, instance.stories_endpoint);
|
||||
assert.notCalled(instance.store.dispatch);
|
||||
assert.called(Components.utils.reportError);
|
||||
});
|
||||
it("should exclude blocked (dismissed) URLs", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
globals.set("NewTabUtils", {blockedLinks: {isBlocked: url => url === "blocked"}});
|
||||
|
||||
const response = `{"list": [{"dedupe_url" : "blocked"}, {"dedupe_url" : "not_blocked"}]}`;
|
||||
instance.stories_endpoint = "stories-endpoint";
|
||||
fetchStub.resolves({ok: true, status: 200, text: () => response});
|
||||
await instance.fetchStories();
|
||||
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_ROWS_UPDATE);
|
||||
assert.equal(instance.store.dispatch.firstCall.args[0].data.rows.length, 1);
|
||||
assert.equal(instance.store.dispatch.firstCall.args[0].data.rows[0].url, "not_blocked");
|
||||
});
|
||||
it("should fetch topics and send event", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
|
||||
const response = `{"topics": [{"name" : "topic1", "url" : "url-topic1"}, {"name" : "topic2", "url" : "url-topic2"}]}`;
|
||||
const topics = [{
|
||||
"name": "topic1",
|
||||
"url": "url-topic1"
|
||||
}, {
|
||||
"name": "topic2",
|
||||
"url": "url-topic2"
|
||||
}];
|
||||
|
||||
instance.topics_endpoint = "topics-endpoint";
|
||||
fetchStub.resolves({ok: true, status: 200, text: () => response});
|
||||
await instance.fetchTopics();
|
||||
|
||||
assert.calledOnce(fetchStub);
|
||||
assert.calledWithExactly(fetchStub, instance.topics_endpoint);
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_ROWS_UPDATE);
|
||||
assert.deepEqual(instance.store.dispatch.firstCall.args[0].data.id, SECTION_ID);
|
||||
assert.deepEqual(instance.store.dispatch.firstCall.args[0].data.topics, topics);
|
||||
});
|
||||
it("should report error for unexpected topics response", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
globals.sandbox.spy(global.Components.utils, "reportError");
|
||||
|
||||
instance.topics_endpoint = "topics-endpoint";
|
||||
fetchStub.resolves({ok: false, status: 400});
|
||||
await instance.fetchTopics();
|
||||
|
||||
assert.calledOnce(fetchStub);
|
||||
assert.calledWithExactly(fetchStub, instance.topics_endpoint);
|
||||
assert.notCalled(instance.store.dispatch);
|
||||
assert.called(Components.utils.reportError);
|
||||
});
|
||||
});
|
||||
describe("#update", () => {
|
||||
it("should fetch stories after update interval", () => {
|
||||
instance.fetchStories = sinon.spy();
|
||||
instance.fetchTopics = sinon.spy();
|
||||
instance.onAction({type: at.SYSTEM_TICK});
|
||||
assert.notCalled(instance.fetchStories);
|
||||
|
||||
clock.tick(STORIES_UPDATE_TIME);
|
||||
instance.onAction({type: at.SYSTEM_TICK});
|
||||
assert.calledOnce(instance.fetchStories);
|
||||
});
|
||||
it("should fetch topics after update interval", () => {
|
||||
instance.fetchStories = sinon.spy();
|
||||
instance.fetchTopics = sinon.spy();
|
||||
instance.onAction({type: at.SYSTEM_TICK});
|
||||
assert.notCalled(instance.fetchTopics);
|
||||
|
||||
clock.tick(TOPICS_UPDATE_TIME);
|
||||
instance.onAction({type: at.SYSTEM_TICK});
|
||||
assert.calledOnce(instance.fetchTopics);
|
||||
});
|
||||
});
|
||||
});
|
@ -24,6 +24,16 @@ describe("initStore", () => {
|
||||
callback(message);
|
||||
assert.calledWith(store.dispatch, message.data);
|
||||
});
|
||||
it("should log errors from failed messages", () => {
|
||||
const callback = global.addMessageListener.firstCall.args[1];
|
||||
globals.sandbox.stub(global.console, "error");
|
||||
globals.sandbox.stub(store, "dispatch").throws(Error("failed"));
|
||||
|
||||
const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: "FOO"}};
|
||||
callback(message);
|
||||
|
||||
assert.calledOnce(global.console.error);
|
||||
});
|
||||
it("should replace the state if a MERGE_STORE_ACTION is dispatched", () => {
|
||||
store.dispatch({type: initStore.MERGE_STORE_ACTION, data: {number: 42}});
|
||||
assert.deepEqual(store.getState(), {number: 42});
|
||||
|
@ -29,6 +29,7 @@ overrider.set({
|
||||
Preferences: FakePrefs,
|
||||
Services: {
|
||||
locale: {getRequestedLocale() {}},
|
||||
urlFormatter: {formatURL: str => str},
|
||||
mm: {
|
||||
addMessageListener: (msg, cb) => cb(),
|
||||
removeMessageListener() {}
|
||||
@ -39,6 +40,8 @@ overrider.set({
|
||||
removeObserver() {}
|
||||
},
|
||||
prefs: {
|
||||
addObserver() {},
|
||||
removeObserver() {},
|
||||
getStringPref() {},
|
||||
getDefaultBranch() {
|
||||
return {
|
||||
|