Bug 1363085 - Part 2. Add telemetry, context menu, react-intl to Activity Stream system add-on. r=ursula
MozReview-Commit-ID: 9ouqxOFvTg4 --HG-- extra : rebase_source : b6c188905a9d339fa362f43cf12dc919577dd4cc
|
@ -3,19 +3,47 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
|
||||
const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
|
||||
this.MAIN_MESSAGE_TYPE = "ActivityStream:Main";
|
||||
this.CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
|
||||
this.UI_CODE = 1;
|
||||
this.BACKGROUND_PROCESS = 2;
|
||||
|
||||
/**
|
||||
* globalImportContext - Are we in UI code (i.e. react, a dom) or some kind of background process?
|
||||
* Use this in action creators if you need different logic
|
||||
* for ui/background processes.
|
||||
*/
|
||||
const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE;
|
||||
// Export for tests
|
||||
this.globalImportContext = globalImportContext;
|
||||
|
||||
const actionTypes = [
|
||||
"BLOCK_URL",
|
||||
"BOOKMARK_URL",
|
||||
"DELETE_BOOKMARK_BY_ID",
|
||||
"DELETE_HISTORY_URL",
|
||||
"INIT",
|
||||
"UNINIT",
|
||||
"LOCALE_UPDATED",
|
||||
"NEW_TAB_INITIAL_STATE",
|
||||
"NEW_TAB_LOAD",
|
||||
"NEW_TAB_UNLOAD",
|
||||
"NEW_TAB_VISIBLE",
|
||||
"OPEN_NEW_WINDOW",
|
||||
"OPEN_PRIVATE_WINDOW",
|
||||
"PERFORM_SEARCH",
|
||||
"PLACES_BOOKMARK_ADDED",
|
||||
"PLACES_BOOKMARK_CHANGED",
|
||||
"PLACES_BOOKMARK_REMOVED",
|
||||
"PLACES_HISTORY_CLEARED",
|
||||
"PLACES_LINK_BLOCKED",
|
||||
"PLACES_LINK_DELETED",
|
||||
"SCREENSHOT_UPDATED",
|
||||
"SEARCH_STATE_UPDATED",
|
||||
"TOP_SITES_UPDATED"
|
||||
"TELEMETRY_PERFORMANCE_EVENT",
|
||||
"TELEMETRY_UNDESIRED_EVENT",
|
||||
"TELEMETRY_USER_EVENT",
|
||||
"TOP_SITES_UPDATED",
|
||||
"UNINIT"
|
||||
// The line below creates an object like this:
|
||||
// {
|
||||
// INIT: "INIT",
|
||||
|
@ -48,14 +76,14 @@ function _RouteMessage(action, options) {
|
|||
*
|
||||
* @param {object} action Any redux action (required)
|
||||
* @param {object} options
|
||||
* @param {string} options.fromTarget The id of the content port from which the action originated. (optional)
|
||||
* @param {string} fromTarget The id of the content port from which the action originated. (optional)
|
||||
* @return {object} An action with added .meta properties
|
||||
*/
|
||||
function SendToMain(action, options = {}) {
|
||||
function SendToMain(action, fromTarget) {
|
||||
return _RouteMessage(action, {
|
||||
from: CONTENT_MESSAGE_TYPE,
|
||||
to: MAIN_MESSAGE_TYPE,
|
||||
fromTarget: options.fromTarget
|
||||
fromTarget
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -90,12 +118,59 @@ function SendToContent(action, target) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UserEvent - A telemetry ping indicating a user action. This should only
|
||||
* be sent from the UI during a user session.
|
||||
*
|
||||
* @param {object} data Fields to include in the ping (source, etc.)
|
||||
* @return {object} An SendToMain action
|
||||
*/
|
||||
function UserEvent(data) {
|
||||
return SendToMain({
|
||||
type: actionTypes.TELEMETRY_USER_EVENT,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UndesiredEvent - A telemetry ping indicating an undesired state.
|
||||
*
|
||||
* @param {object} data Fields to include in the ping (value, etc.)
|
||||
* @param {int} importContext (For testing) Override the import context for testing.
|
||||
* @return {object} An action. For UI code, a SendToMain action.
|
||||
*/
|
||||
function UndesiredEvent(data, importContext = globalImportContext) {
|
||||
const action = {
|
||||
type: actionTypes.TELEMETRY_UNDESIRED_EVENT,
|
||||
data
|
||||
};
|
||||
return importContext === UI_CODE ? SendToMain(action) : action;
|
||||
}
|
||||
|
||||
/**
|
||||
* PerfEvent - A telemetry ping indicating a performance-related event.
|
||||
*
|
||||
* @param {object} data Fields to include in the ping (value, etc.)
|
||||
* @param {int} importContext (For testing) Override the import context for testing.
|
||||
* @return {object} An action. For UI code, a SendToMain action.
|
||||
*/
|
||||
function PerfEvent(data, importContext = globalImportContext) {
|
||||
const action = {
|
||||
type: actionTypes.TELEMETRY_PERFORMANCE_EVENT,
|
||||
data
|
||||
};
|
||||
return importContext === UI_CODE ? SendToMain(action) : action;
|
||||
}
|
||||
|
||||
this.actionTypes = actionTypes;
|
||||
|
||||
this.actionCreators = {
|
||||
SendToMain,
|
||||
BroadcastToContent,
|
||||
UserEvent,
|
||||
UndesiredEvent,
|
||||
PerfEvent,
|
||||
SendToContent,
|
||||
BroadcastToContent
|
||||
SendToMain
|
||||
};
|
||||
|
||||
// These are helpers to test for certain kinds of actions
|
||||
|
@ -124,6 +199,9 @@ this.actionUtils = {
|
|||
}
|
||||
return false;
|
||||
},
|
||||
getPortIdOfSender(action) {
|
||||
return (action.meta && action.meta.fromTarget) || null;
|
||||
},
|
||||
_RouteMessage
|
||||
};
|
||||
|
||||
|
@ -131,6 +209,9 @@ this.EXPORTED_SYMBOLS = [
|
|||
"actionTypes",
|
||||
"actionCreators",
|
||||
"actionUtils",
|
||||
"globalImportContext",
|
||||
"UI_CODE",
|
||||
"BACKGROUND_PROCESS",
|
||||
"MAIN_MESSAGE_TYPE",
|
||||
"CONTENT_MESSAGE_TYPE"
|
||||
];
|
||||
|
|
|
@ -6,20 +6,52 @@
|
|||
const {actionTypes: at} = Components.utils.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
const INITIAL_STATE = {
|
||||
App: {
|
||||
// Have we received real data from the app yet?
|
||||
initialized: false,
|
||||
// The locale of the browser
|
||||
locale: "",
|
||||
// Localized strings with defaults
|
||||
strings: {},
|
||||
// The version of the system-addon
|
||||
version: null
|
||||
},
|
||||
TopSites: {
|
||||
init: false,
|
||||
// Have we received real data from history yet?
|
||||
initialized: false,
|
||||
// The history (and possibly default) links
|
||||
rows: []
|
||||
},
|
||||
Search: {
|
||||
// The search engine currently set by the browser
|
||||
currentEngine: {
|
||||
name: "",
|
||||
icon: ""
|
||||
},
|
||||
// All possible search engines
|
||||
engines: []
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Handle some real actions here, once we have a TopSites feed working
|
||||
function App(prevState = INITIAL_STATE.App, action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
return Object.assign({}, action.data || {}, {initialized: true});
|
||||
case at.LOCALE_UPDATED: {
|
||||
if (!action.data) {
|
||||
return prevState;
|
||||
}
|
||||
let {locale, strings} = action.data;
|
||||
return Object.assign({}, prevState, {
|
||||
locale,
|
||||
strings
|
||||
});
|
||||
}
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
}
|
||||
|
||||
function TopSites(prevState = INITIAL_STATE.TopSites, action) {
|
||||
let hasMatch;
|
||||
let newRows;
|
||||
|
@ -28,7 +60,7 @@ function TopSites(prevState = INITIAL_STATE.TopSites, action) {
|
|||
if (!action.data) {
|
||||
return prevState;
|
||||
}
|
||||
return Object.assign({}, prevState, {init: true, rows: action.data});
|
||||
return Object.assign({}, prevState, {initialized: true, rows: action.data});
|
||||
case at.SCREENSHOT_UPDATED:
|
||||
newRows = prevState.rows.map(row => {
|
||||
if (row.url === action.data.url) {
|
||||
|
@ -38,6 +70,31 @@ function TopSites(prevState = INITIAL_STATE.TopSites, action) {
|
|||
return row;
|
||||
});
|
||||
return hasMatch ? Object.assign({}, prevState, {rows: newRows}) : prevState;
|
||||
case at.PLACES_BOOKMARK_ADDED:
|
||||
newRows = prevState.rows.map(site => {
|
||||
if (site.url === action.data.url) {
|
||||
const {bookmarkGuid, bookmarkTitle, lastModified} = action.data;
|
||||
return Object.assign({}, site, {bookmarkGuid, bookmarkTitle, bookmarkDateCreated: lastModified});
|
||||
}
|
||||
return site;
|
||||
});
|
||||
return Object.assign({}, prevState, {rows: newRows});
|
||||
case at.PLACES_BOOKMARK_REMOVED:
|
||||
newRows = prevState.rows.map(site => {
|
||||
if (site.url === action.data.url) {
|
||||
const newSite = Object.assign({}, site);
|
||||
delete newSite.bookmarkGuid;
|
||||
delete newSite.bookmarkTitle;
|
||||
delete newSite.bookmarkDateCreated;
|
||||
return newSite;
|
||||
}
|
||||
return site;
|
||||
});
|
||||
return Object.assign({}, prevState, {rows: newRows});
|
||||
case at.PLACES_LINK_DELETED:
|
||||
case at.PLACES_LINK_BLOCKED:
|
||||
newRows = prevState.rows.filter(val => val.url !== action.data.url);
|
||||
return Object.assign({}, prevState, {rows: newRows});
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
|
@ -60,6 +117,6 @@ function Search(prevState = INITIAL_STATE.Search, action) {
|
|||
}
|
||||
}
|
||||
this.INITIAL_STATE = INITIAL_STATE;
|
||||
this.reducers = {TopSites, Search};
|
||||
this.reducers = {TopSites, App, Search};
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"];
|
||||
|
|
|
@ -6,6 +6,9 @@ html {
|
|||
*::after {
|
||||
box-sizing: inherit; }
|
||||
|
||||
*::-moz-focus-inner {
|
||||
border: 0; }
|
||||
|
||||
body {
|
||||
margin: 0; }
|
||||
|
||||
|
@ -17,6 +20,29 @@ input {
|
|||
[hidden] {
|
||||
display: none !important; }
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-size: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
vertical-align: middle; }
|
||||
.icon.icon-spacer {
|
||||
margin-inline-end: 8px; }
|
||||
.icon.icon-bookmark {
|
||||
background-image: url("assets/glyph-bookmark-16.svg"); }
|
||||
.icon.icon-bookmark-remove {
|
||||
background-image: url("assets/glyph-bookmark-remove-16.svg"); }
|
||||
.icon.icon-delete {
|
||||
background-image: url("assets/glyph-delete-16.svg"); }
|
||||
.icon.icon-dismiss {
|
||||
background-image: url("assets/glyph-dismiss-16.svg"); }
|
||||
.icon.icon-new-window {
|
||||
background-image: url("assets/glyph-newWindow-16.svg"); }
|
||||
.icon.icon-new-window-private {
|
||||
background-image: url("assets/glyph-newWindow-private-16.svg"); }
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
|
@ -134,49 +160,81 @@ main {
|
|||
display: inline-block;
|
||||
margin: 0 0 18px;
|
||||
margin-inline-end: 32px; }
|
||||
.top-sites-list a {
|
||||
display: block;
|
||||
color: inherit; }
|
||||
.top-sites-list .tile {
|
||||
position: relative;
|
||||
height: 96px;
|
||||
width: 96px;
|
||||
border-radius: 6px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
color: #A0A0A0;
|
||||
font-weight: 200;
|
||||
font-size: 32px;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center; }
|
||||
.top-sites-list .tile:hover {
|
||||
.top-sites-list .top-site-outer {
|
||||
position: relative; }
|
||||
.top-sites-list .top-site-outer > a {
|
||||
display: block;
|
||||
color: inherit;
|
||||
outline: none; }
|
||||
.top-sites-list .top-site-outer > a.active .tile, .top-sites-list .top-site-outer > a:focus .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 .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; }
|
||||
.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 {
|
||||
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 .screenshot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #FFF;
|
||||
border-radius: 6px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
background-size: 250%;
|
||||
background-position: top left;
|
||||
transition: opacity 1s;
|
||||
opacity: 0; }
|
||||
.top-sites-list .screenshot.active {
|
||||
.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 {
|
||||
transform: scale(1);
|
||||
opacity: 1; }
|
||||
.top-sites-list .title {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 96px; }
|
||||
.top-sites-list .top-site-outer .tile {
|
||||
position: relative;
|
||||
height: 96px;
|
||||
width: 96px;
|
||||
border-radius: 6px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
color: #A0A0A0;
|
||||
font-weight: 200;
|
||||
font-size: 32px;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center; }
|
||||
.top-sites-list .top-site-outer .screenshot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #FFF;
|
||||
border-radius: 6px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
background-size: 250%;
|
||||
background-position: top left;
|
||||
transition: opacity 1s;
|
||||
opacity: 0; }
|
||||
.top-sites-list .top-site-outer .screenshot.active {
|
||||
opacity: 1; }
|
||||
.top-sites-list .top-site-outer .title {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 96px; }
|
||||
|
||||
.search-wrapper {
|
||||
cursor: default;
|
||||
|
@ -291,11 +349,9 @@ main {
|
|||
.search-wrapper input:focus {
|
||||
border-color: #0996F8;
|
||||
box-shadow: 0 0 0 2px #0996F8;
|
||||
transition: box-shadow 150ms;
|
||||
z-index: 1; }
|
||||
.search-wrapper input:focus + button {
|
||||
z-index: 1;
|
||||
transition: box-shadow 150ms;
|
||||
box-shadow: 0 0 0 2px #0996F8;
|
||||
background-color: #0996F8;
|
||||
background-image: url("assets/glyph-forward-16-white.svg");
|
||||
|
@ -319,16 +375,52 @@ main {
|
|||
border: 0;
|
||||
width: 36px;
|
||||
padding: 0;
|
||||
transition: box-shadow 150ms;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
|
||||
background: #FFF url("assets/glyph-forward-16.svg") no-repeat center center;
|
||||
background-size: 16px 16px; }
|
||||
.search-wrapper button:hover {
|
||||
z-index: 1;
|
||||
transition: box-shadow 150ms;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 1, 0.5);
|
||||
background-color: #0996F8;
|
||||
background-image: url("assets/glyph-forward-16-white.svg");
|
||||
color: #FFF; }
|
||||
.search-wrapper button:dir(rtl) {
|
||||
transform: scaleX(-1); }
|
||||
|
||||
.context-menu {
|
||||
display: block;
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
top: 6.75px;
|
||||
offset-inline-start: 100%;
|
||||
margin-inline-start: 5px;
|
||||
z-index: 10000;
|
||||
background: #FBFBFB;
|
||||
border-radius: 5px; }
|
||||
.context-menu > ul {
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
list-style: none; }
|
||||
.context-menu > ul > li {
|
||||
margin: 0;
|
||||
width: 100%; }
|
||||
.context-menu > ul > li.separator {
|
||||
margin: 5px 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.2); }
|
||||
.context-menu > ul > li > a {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
white-space: nowrap;
|
||||
padding: 3px 12px;
|
||||
line-height: 16px;
|
||||
display: flex;
|
||||
align-items: center; }
|
||||
.context-menu > ul > li > a:hover, .context-menu > ul > li > a:focus {
|
||||
background: #2B99FF;
|
||||
color: #FFF; }
|
||||
.context-menu > ul > li > a:hover a, .context-menu > ul > li > a:focus a {
|
||||
color: #383E49; }
|
||||
.context-menu > ul > li > a:hover:hover, .context-menu > ul > li > a:hover:focus, .context-menu > ul > li > a:focus:hover, .context-menu > ul > li > a:focus:focus {
|
||||
color: #FFF; }
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
<html lang="en-us" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>New Tab</title>
|
||||
<title></title>
|
||||
<link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
|
||||
</head>
|
||||
<body class="activity-stream">
|
||||
<div id="root"></div>
|
||||
<script src="resource://activity-stream/vendor/react.js"></script>
|
||||
<script src="resource://activity-stream/vendor/react-dom.js"></script>
|
||||
<script src="resource://activity-stream/vendor/react-intl.js"></script>
|
||||
<script src="resource://activity-stream/vendor/redux.js"></script>
|
||||
<script src="resource://activity-stream/vendor/react-redux.js"></script>
|
||||
<script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- 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" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<style>
|
||||
path {
|
||||
fill-rule: evenodd;
|
||||
fill:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
<path d="M198.992,18a0.955,0.955,0,0,0-.772.651l-1.984,4.122-4.332.72a0.851,0.851,0,0,0-.53,1.563l3.112,3.262-0.69,4.589c-0.1.69,0.173,1.094,0.658,1.094a1.4,1.4,0,0,0,.635-0.181l3.9-2.075,3.9,2.075a1.4,1.4,0,0,0,.634.181c0.485,0,.761-0.4.659-1.094L203.5,28.317l3.108-3.259a0.853,0.853,0,0,0-.53-1.566l-4.3-.719-2.016-4.122A0.953,0.953,0,0,0,198.992,18h0Z" transform="translate(-191 -18)"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 844 B |
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- 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" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<style>
|
||||
path {
|
||||
fill-rule: evenodd;
|
||||
fill:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
<path d="M199.008,47.642l0.983,2.01,0.452,0.924,1.015,0.17,2.324,0.389-1.719,1.8-0.676.708,0.145,0.968,0.36,2.4-1.953-1.038-0.938-.5-0.939.5-1.951,1.037,0.36-2.4,0.146-.969-0.676-.709-1.718-1.8,2.349-.39,1.024-.17,0.45-.935,0.962-2M199,44a0.953,0.953,0,0,0-.772.651l-1.984,4.122-4.332.72a0.851,0.851,0,0,0-.53,1.563l3.112,3.262-0.69,4.589c-0.1.69,0.172,1.094,0.658,1.094a1.394,1.394,0,0,0,.634-0.181L199,57.744l3.9,2.075a1.4,1.4,0,0,0,.635.181c0.485,0,.761-0.4.658-1.094l-0.687-4.589,3.108-3.259a0.853,0.853,0,0,0-.53-1.566l-4.3-.72-2.016-4.122A0.953,0.953,0,0,0,199,44h0Z" transform="translate(-191 -44)"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.0 KiB |
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- 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" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<style>
|
||||
path {
|
||||
fill-rule: evenodd;
|
||||
fill:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
<path d="M426,22H416a1,1,0,0,1,0-2h3a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1h3A1,1,0,0,1,426,22Zm-0.9,10a1.132,1.132,0,0,1-1.1,1H418a1.125,1.125,0,0,1-1.1-1L416,23h10Z" transform="translate(-413 -18)"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 647 B |
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- 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" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<style>
|
||||
path {
|
||||
fill-rule: evenodd;
|
||||
fill:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
<path d="M422.414,52l3.531-3.531a1,1,0,1,0-1.414-1.414L421,50.586l-3.531-3.531a1,1,0,1,0-1.414,1.414L419.586,52l-3.531,3.531a1,1,0,1,0,1.414,1.414L421,53.414l3.531,3.531a1,1,0,1,0,1.414-1.414Z" transform="translate(-413 -44)"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 682 B |
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<style>
|
||||
circle {
|
||||
fill: #4d4d4d;
|
||||
}
|
||||
</style>
|
||||
|
||||
<g>
|
||||
<circle cx="2" cy="8" r="2"/>
|
||||
<circle cx="7" cy="8" r="2"/>
|
||||
<circle cx="12" cy="8" r="2"/>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 352 B |
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- 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" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<style>
|
||||
path {
|
||||
fill-rule: evenodd;
|
||||
fill:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
<path d="M382,20.007A1,1,0,0,1,383,19h14a1,1,0,0,1,1,1.007V31.993A1,1,0,0,1,397,33H383a1,1,0,0,1-1-1.007V20.007ZM384,23h12v8H384V23Zm0.5-3a0.5,0.5,0,1,1-.5.5A0.5,0.5,0,0,1,384.5,20Zm2,0a0.5,0.5,0,1,1-.5.5A0.5,0.5,0,0,1,386.5,20Zm2,0a0.5,0.5,0,1,1-.5.5A0.5,0.5,0,0,1,388.5,20Z" transform="translate(-382 -18)"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 765 B |
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- 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" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<style>
|
||||
path {
|
||||
fill-rule: evenodd;
|
||||
fill:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
<path d="M356.994,24.619c-1.954.47-1.714,1.625-1.714,1.625s2.264,0.849,3.368.258a8.76,8.76,0,0,0,1.167-.668s-1.493-1.534-2.821-1.215m-5.987,0c-1.328-.32-2.821,1.215-2.821,1.215a8.76,8.76,0,0,0,1.167.668c1.1,0.591,3.368-.258,3.368-0.258s0.24-1.155-1.714-1.625M362,24.667c0,2.006-.647,5.334-3.755,5.333-1.143,0-3.1-1.993-4.245-1.993S350.9,30,349.755,30C346.647,30,346,26.673,346,24.667c0-2.094.984-2.813,3.628-2.638,2.739,0.181,3.066,1.087,4.372,1.087s1.8-.906,4.373-1.087c2.713-.191,3.627.544,3.627,2.638" transform="translate(-346 -18)"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 993 B |
|
@ -9,6 +9,7 @@
|
|||
content/vendor/Redux.jsm (./vendor/Redux.jsm)
|
||||
content/vendor/react.js (./vendor/react.js)
|
||||
content/vendor/react-dom.js (./vendor/react-dom.js)
|
||||
content/vendor/react-intl.js (./vendor/react-intl.js)
|
||||
content/vendor/redux.js (./vendor/redux.js)
|
||||
content/vendor/react-redux.js (./vendor/react-redux.js)
|
||||
content/data/ (./data/*)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
/* 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/. */
|
||||
/* globals XPCOMUtils, NewTabInit, TopSitesFeed, SearchFeed */
|
||||
|
||||
/* globals LocalizationFeed, NewTabInit, SearchFeed, TelemetryFeed, TopSitesFeed, XPCOMUtils */
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
|
@ -11,13 +10,24 @@ const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
|
|||
const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
// Feeds
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LocalizationFeed",
|
||||
"resource://activity-stream/lib/LocalizationFeed.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NewTabInit",
|
||||
"resource://activity-stream/lib/NewTabInit.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TopSitesFeed",
|
||||
"resource://activity-stream/lib/TopSitesFeed.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesFeed",
|
||||
"resource://activity-stream/lib/PlacesFeed.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SearchFeed",
|
||||
"resource://activity-stream/lib/SearchFeed.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFeed",
|
||||
"resource://activity-stream/lib/TelemetryFeed.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TopSitesFeed",
|
||||
"resource://activity-stream/lib/TopSitesFeed.jsm");
|
||||
|
||||
const feeds = {
|
||||
// When you add a feed here:
|
||||
// 1. The key in this object should directly refer to a pref, not including the
|
||||
|
@ -26,9 +36,12 @@ const feeds = {
|
|||
// 2. The value should be a function that returns a feed.
|
||||
// 3. You should use XPCOMUtils.defineLazyModuleGetter to import the Feed,
|
||||
// so it isn't loaded until the feed is enabled.
|
||||
"feeds.localization": () => new LocalizationFeed(),
|
||||
"feeds.newtabinit": () => new NewTabInit(),
|
||||
"feeds.topsites": () => new TopSitesFeed(),
|
||||
"feeds.search": () => new SearchFeed()
|
||||
"feeds.places": () => new PlacesFeed(),
|
||||
"feeds.search": () => new SearchFeed(),
|
||||
"feeds.telemetry": () => new TelemetryFeed(),
|
||||
"feeds.topsites": () => new TopSitesFeed()
|
||||
};
|
||||
|
||||
this.ActivityStream = class ActivityStream {
|
||||
|
@ -41,7 +54,7 @@ this.ActivityStream = class ActivityStream {
|
|||
* @param {string} options.version Version of the add-on. e.g. "0.1.0"
|
||||
* @param {string} options.newTabURL URL of New Tab page on which A.S. is displayed. e.g. "about:newtab"
|
||||
*/
|
||||
constructor(options) {
|
||||
constructor(options = {}) {
|
||||
this.initialized = false;
|
||||
this.options = options;
|
||||
this.store = new Store();
|
||||
|
@ -50,7 +63,10 @@ this.ActivityStream = class ActivityStream {
|
|||
init() {
|
||||
this.initialized = true;
|
||||
this.store.init(this.feeds);
|
||||
this.store.dispatch({type: at.INIT});
|
||||
this.store.dispatch({
|
||||
type: at.INIT,
|
||||
data: {version: this.options.version}
|
||||
});
|
||||
}
|
||||
uninit() {
|
||||
this.store.dispatch({type: at.UNINIT});
|
||||
|
|
|
@ -89,7 +89,7 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
|
|||
* @param {string} targetId The portID of the port that sent the message
|
||||
*/
|
||||
onActionFromContent(action, targetId) {
|
||||
this.dispatch(ac.SendToMain(action, {fromTarget: targetId}));
|
||||
this.dispatch(ac.SendToMain(action, targetId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,7 +196,7 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
|
|||
action._target = msg.target;
|
||||
this.onActionFromContent(action, portID);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
||||
this.EXPORTED_SYMBOLS = ["ActivityStreamMessageChannel", "DEFAULT_OPTIONS"];
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/* 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/. */
|
||||
/* globals Services, XPCOMUtils */
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
Cu.importGlobalProperties(["fetch"]);
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||
"resource://gre/modules/Services.jsm");
|
||||
|
||||
// What is our default locale for the app?
|
||||
const DEFAULT_LOCALE = "en-US";
|
||||
// Event from LocaleService when locales are assigned
|
||||
const LOCALES_CHANGE_TOPIC = "intl:requested-locales-changed";
|
||||
// Where is the packaged locales json with all strings?
|
||||
const LOCALES_FILE = "resource://activity-stream/data/locales.json";
|
||||
|
||||
this.LocalizationFeed = class LocalizationFeed {
|
||||
async init() {
|
||||
Services.obs.addObserver(this, LOCALES_CHANGE_TOPIC);
|
||||
|
||||
let response = await fetch(LOCALES_FILE);
|
||||
this.allStrings = await response.json();
|
||||
|
||||
this.updateLocale();
|
||||
}
|
||||
uninit() {
|
||||
Services.obs.removeObserver(this, LOCALES_CHANGE_TOPIC);
|
||||
}
|
||||
|
||||
updateLocale() {
|
||||
let locale = Services.locale.getRequestedLocale() || DEFAULT_LOCALE;
|
||||
let strings = this.allStrings[locale];
|
||||
|
||||
// Use the default strings for any that are missing
|
||||
if (locale !== DEFAULT_LOCALE) {
|
||||
strings = Object.assign({}, this.allStrings[DEFAULT_LOCALE], strings || {});
|
||||
}
|
||||
|
||||
this.store.dispatch(ac.BroadcastToContent({
|
||||
type: at.LOCALE_UPDATED,
|
||||
data: {
|
||||
locale,
|
||||
strings
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case LOCALES_CHANGE_TOPIC:
|
||||
this.updateLocale();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onAction(action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
this.init();
|
||||
break;
|
||||
case at.UNINIT:
|
||||
this.uninit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["LocalizationFeed"];
|
|
@ -0,0 +1,213 @@
|
|||
/* 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/. */
|
||||
/* globals ContentSearch, XPCOMUtils, PlacesUtils, NewTabUtils, Services */
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu, interfaces: Ci} = Components;
|
||||
const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
|
||||
"resource://gre/modules/NewTabUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||
"resource://gre/modules/Services.jsm");
|
||||
|
||||
const LINK_BLOCKED_EVENT = "newtab-linkBlocked";
|
||||
|
||||
/**
|
||||
* Observer - a wrapper around history/bookmark observers to add the QueryInterface.
|
||||
*/
|
||||
class Observer {
|
||||
constructor(dispatch, observerInterface) {
|
||||
this.dispatch = dispatch;
|
||||
this.QueryInterface = XPCOMUtils.generateQI([observerInterface, Ci.nsISupportsWeakReference]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HistoryObserver - observes events from PlacesUtils.history
|
||||
*/
|
||||
class HistoryObserver extends Observer {
|
||||
constructor(dispatch) {
|
||||
super(dispatch, Ci.nsINavHistoryObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* onDeleteURI - Called when an link is deleted from history.
|
||||
*
|
||||
* @param {obj} uri A URI object representing the link's url
|
||||
* {str} uri.spec The URI as a string
|
||||
*/
|
||||
onDeleteURI(uri) {
|
||||
this.dispatch({
|
||||
type: at.PLACES_LINK_DELETED,
|
||||
data: {url: uri.spec}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* onClearHistory - Called when the user clears their entire history.
|
||||
*/
|
||||
onClearHistory() {
|
||||
this.dispatch({type: at.PLACES_HISTORY_CLEARED});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BookmarksObserver - observes events from PlacesUtils.bookmarks
|
||||
*/
|
||||
class BookmarksObserver extends Observer {
|
||||
constructor(dispatch) {
|
||||
super(dispatch, Ci.nsINavBookmarkObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* onItemAdded - Called when a bookmark is added
|
||||
*
|
||||
* @param {str} id
|
||||
* @param {str} folderId
|
||||
* @param {int} index
|
||||
* @param {int} type Indicates if the bookmark is an actual bookmark,
|
||||
* a folder, or a separator.
|
||||
* @param {str} uri
|
||||
* @param {str} title
|
||||
* @param {int} dateAdded
|
||||
* @param {str} guid The unique id of the bookmark
|
||||
*/
|
||||
async onItemAdded(...args) {
|
||||
const type = args[3];
|
||||
const guid = args[7];
|
||||
if (type !== PlacesUtils.bookmarks.TYPE_BOOKMARK) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// bookmark: {bookmarkGuid, bookmarkTitle, lastModified, url}
|
||||
const bookmark = await NewTabUtils.activityStreamProvider.getBookmark(guid);
|
||||
this.dispatch({type: at.PLACES_BOOKMARK_ADDED, data: bookmark});
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onItemRemoved - Called when a bookmark is removed
|
||||
*
|
||||
* @param {str} id
|
||||
* @param {str} folderId
|
||||
* @param {int} index
|
||||
* @param {int} type Indicates if the bookmark is an actual bookmark,
|
||||
* a folder, or a separator.
|
||||
* @param {str} uri
|
||||
* @param {str} guid The unique id of the bookmark
|
||||
*/
|
||||
onItemRemoved(id, folderId, index, type, uri, guid) {
|
||||
if (type === PlacesUtils.bookmarks.TYPE_BOOKMARK) {
|
||||
this.dispatch({
|
||||
type: at.PLACES_BOOKMARK_REMOVED,
|
||||
data: {url: uri.spec, bookmarkGuid: guid}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onItemChanged - Called when a bookmark is modified
|
||||
*
|
||||
* @param {str} id description
|
||||
* @param {str} property The property that was modified (e.g. uri, title)
|
||||
* @param {bool} isAnnotation
|
||||
* @param {any} value
|
||||
* @param {int} lastModified
|
||||
* @param {int} type Indicates if the bookmark is an actual bookmark,
|
||||
* a folder, or a separator.
|
||||
* @param {int} parent
|
||||
* @param {str} guid The unique id of the bookmark
|
||||
*/
|
||||
async onItemChanged(...args) {
|
||||
const property = args[1];
|
||||
const type = args[5];
|
||||
const guid = args[7];
|
||||
|
||||
// Only process this event if it is a TYPE_BOOKMARK, and uri or title was the property changed.
|
||||
if (type !== PlacesUtils.bookmarks.TYPE_BOOKMARK || !["uri", "title"].includes(property)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// bookmark: {bookmarkGuid, bookmarkTitle, lastModified, url}
|
||||
const bookmark = await NewTabUtils.activityStreamProvider.getBookmark(guid);
|
||||
this.dispatch({type: at.PLACES_BOOKMARK_CHANGED, data: bookmark});
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PlacesFeed {
|
||||
constructor() {
|
||||
this.historyObserver = new HistoryObserver(action => this.store.dispatch(ac.BroadcastToContent(action)));
|
||||
this.bookmarksObserver = new BookmarksObserver(action => this.store.dispatch(ac.BroadcastToContent(action)));
|
||||
}
|
||||
|
||||
addObservers() {
|
||||
PlacesUtils.history.addObserver(this.historyObserver, true);
|
||||
PlacesUtils.bookmarks.addObserver(this.bookmarksObserver, true);
|
||||
Services.obs.addObserver(this, LINK_BLOCKED_EVENT);
|
||||
}
|
||||
|
||||
removeObservers() {
|
||||
PlacesUtils.history.removeObserver(this.historyObserver);
|
||||
PlacesUtils.bookmarks.removeObserver(this.bookmarksObserver);
|
||||
Services.obs.removeObserver(this, LINK_BLOCKED_EVENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* observe - An observer for the LINK_BLOCKED_EVENT.
|
||||
* Called when a link is blocked.
|
||||
*
|
||||
* @param {null} subject
|
||||
* @param {str} topic The name of the event
|
||||
* @param {str} value The data associated with the event
|
||||
*/
|
||||
observe(subject, topic, value) {
|
||||
if (topic === LINK_BLOCKED_EVENT) {
|
||||
this.store.dispatch(ac.BroadcastToContent({
|
||||
type: at.PLACES_LINK_BLOCKED,
|
||||
data: {url: value}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
onAction(action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
this.addObservers();
|
||||
break;
|
||||
case at.UNINIT:
|
||||
this.removeObservers();
|
||||
break;
|
||||
case at.BLOCK_URL:
|
||||
NewTabUtils.activityStreamLinks.blockURL({url: action.data});
|
||||
break;
|
||||
case at.BOOKMARK_URL:
|
||||
NewTabUtils.activityStreamLinks.addBookmark(action.data);
|
||||
break;
|
||||
case at.DELETE_BOOKMARK_BY_ID:
|
||||
NewTabUtils.activityStreamLinks.deleteBookmark(action.data);
|
||||
break;
|
||||
case at.DELETE_HISTORY_URL:
|
||||
NewTabUtils.activityStreamLinks.deleteHistoryEntry(action.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.PlacesFeed = PlacesFeed;
|
||||
|
||||
// Exported for testing only
|
||||
PlacesFeed.HistoryObserver = HistoryObserver;
|
||||
PlacesFeed.BookmarksObserver = BookmarksObserver;
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PlacesFeed"];
|
|
@ -18,10 +18,19 @@ XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
|||
this.SearchFeed = class SearchFeed {
|
||||
addObservers() {
|
||||
Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC);
|
||||
|
||||
// Notice when ContentSearch.init would be lazily loaded from nsBrowserGlue
|
||||
this.contentSearch = new Promise(resolve => Services.mm.addMessageListener(
|
||||
"ContentSearch", (this._onMessage = () => {
|
||||
Services.mm.removeMessageListener("ContentSearch", this._onMessage);
|
||||
resolve(ContentSearch);
|
||||
})));
|
||||
}
|
||||
removeObservers() {
|
||||
Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
|
||||
Services.mm.removeMessageListener("ContentSearch", this._onMessage);
|
||||
}
|
||||
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case SEARCH_ENGINE_TOPIC:
|
||||
|
@ -31,8 +40,10 @@ this.SearchFeed = class SearchFeed {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async getState() {
|
||||
const state = await ContentSearch.currentStateObj(true);
|
||||
// Wait for ContentSearch to be lazily loaded before getting state
|
||||
const state = await (await this.contentSearch).currentStateObj(true);
|
||||
const engines = state.engines.map(engine => ({
|
||||
name: engine.name,
|
||||
icon: engine.iconBuffer
|
||||
|
@ -47,11 +58,12 @@ this.SearchFeed = class SearchFeed {
|
|||
performSearch(browser, data) {
|
||||
ContentSearch.performSearch({target: browser}, data);
|
||||
}
|
||||
onAction(action) {
|
||||
|
||||
async onAction(action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
this.addObservers();
|
||||
this.getState();
|
||||
await this.getState();
|
||||
break;
|
||||
case at.PERFORM_SEARCH:
|
||||
this.performSearch(action._target.browser, action.data);
|
||||
|
|
|
@ -31,9 +31,9 @@ this.Store = class Store {
|
|||
// Bind each redux method so we can call it directly from the Store. E.g.,
|
||||
// store.dispatch() will call store._store.dispatch();
|
||||
["dispatch", "getState", "subscribe"].forEach(method => {
|
||||
this[method] = (...args) => {
|
||||
this[method] = function(...args) {
|
||||
return this._store[method](...args);
|
||||
};
|
||||
}.bind(this);
|
||||
});
|
||||
this.feeds = new Map();
|
||||
this._feedFactories = null;
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
/* 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/. */
|
||||
/* globals XPCOMUtils, gUUIDGenerator, ClientID */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
const {actionTypes: at, actionUtils: au} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
Cu.import("resource://gre/modules/ClientID.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySender",
|
||||
"resource://activity-stream/lib/TelemetrySender.jsm");
|
||||
|
||||
this.TelemetryFeed = class TelemetryFeed {
|
||||
constructor(options) {
|
||||
this.sessions = new Map();
|
||||
this.telemetryClientId = null;
|
||||
this.telemetrySender = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
// TelemetrySender adds pref observers, so we initialize it after INIT
|
||||
this.telemetrySender = new TelemetrySender();
|
||||
|
||||
const id = await ClientID.getClientID();
|
||||
this.telemetryClientId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* addSession - Start tracking a new session
|
||||
*
|
||||
* @param {string} id the portID of the open session
|
||||
*/
|
||||
addSession(id) {
|
||||
this.sessions.set(id, {
|
||||
start_time: Components.utils.now(),
|
||||
session_id: String(gUUIDGenerator.generateUUID()),
|
||||
page: "about:newtab" // TODO: Handle about:home
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* endSession - Stop tracking a session
|
||||
*
|
||||
* @param {string} portID the portID of the session that just closed
|
||||
*/
|
||||
endSession(portID) {
|
||||
const session = this.sessions.get(portID);
|
||||
|
||||
if (!session) {
|
||||
// It's possible the tab was never visible – in which case, there was no user session.
|
||||
return;
|
||||
}
|
||||
|
||||
session.session_duration = Math.round(Components.utils.now() - session.start_time);
|
||||
this.sendEvent(this.createSessionEndEvent(session));
|
||||
this.sessions.delete(portID);
|
||||
}
|
||||
|
||||
/**
|
||||
* createPing - Create a ping with common properties
|
||||
*
|
||||
* @param {string} id The portID of the session, if a session is relevant (optional)
|
||||
* @return {obj} A telemetry ping
|
||||
*/
|
||||
createPing(portID) {
|
||||
const appInfo = this.store.getState().App;
|
||||
const ping = {
|
||||
client_id: this.telemetryClientId,
|
||||
addon_version: appInfo.version,
|
||||
locale: appInfo.locale
|
||||
};
|
||||
|
||||
// If the ping is part of a user session, add session-related info
|
||||
if (portID) {
|
||||
const session = this.sessions.get(portID);
|
||||
Object.assign(ping, {
|
||||
session_id: session.session_id,
|
||||
page: session.page
|
||||
});
|
||||
}
|
||||
return ping;
|
||||
}
|
||||
|
||||
createUserEvent(action) {
|
||||
return Object.assign(
|
||||
this.createPing(au.getPortIdOfSender(action)),
|
||||
action.data,
|
||||
{action: "activity_stream_user_event"}
|
||||
);
|
||||
}
|
||||
|
||||
createUndesiredEvent(action) {
|
||||
return Object.assign(
|
||||
this.createPing(au.getPortIdOfSender(action)),
|
||||
{value: 0}, // Default value
|
||||
action.data,
|
||||
{action: "activity_stream_undesired_event"}
|
||||
);
|
||||
}
|
||||
|
||||
createPerformanceEvent(action) {
|
||||
return Object.assign(
|
||||
this.createPing(au.getPortIdOfSender(action)),
|
||||
action.data,
|
||||
{action: "activity_stream_performance_event"}
|
||||
);
|
||||
}
|
||||
|
||||
createSessionEndEvent(session) {
|
||||
return Object.assign(
|
||||
this.createPing(),
|
||||
{
|
||||
session_id: session.session_id,
|
||||
page: session.page,
|
||||
session_duration: session.session_duration,
|
||||
action: "activity_stream_session"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
sendEvent(event) {
|
||||
this.telemetrySender.sendPing(event);
|
||||
}
|
||||
|
||||
onAction(action) {
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
this.init();
|
||||
break;
|
||||
case at.NEW_TAB_VISIBLE:
|
||||
this.addSession(au.getPortIdOfSender(action));
|
||||
break;
|
||||
case at.NEW_TAB_UNLOAD:
|
||||
this.endSession(au.getPortIdOfSender(action));
|
||||
break;
|
||||
case at.TELEMETRY_UNDESIRED_EVENT:
|
||||
this.sendEvent(this.createUndesiredEvent(action));
|
||||
break;
|
||||
case at.TELEMETRY_USER_EVENT:
|
||||
this.sendEvent(this.createUserEvent(action));
|
||||
break;
|
||||
case at.TELEMETRY_PERFORMANCE_EVENT:
|
||||
this.sendEvent(this.createPerformanceEvent(action));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uninit() {
|
||||
this.telemetrySender.uninit();
|
||||
this.telemetrySender = null;
|
||||
// TODO: Send any unfinished sessions
|
||||
}
|
||||
};
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["TelemetryFeed"];
|
|
@ -0,0 +1,99 @@
|
|||
/* 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/. */
|
||||
/* globals Preferences, Services, XPCOMUtils */
|
||||
|
||||
const {interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.importGlobalProperties(["fetch"]);
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Console.jsm"); // eslint-disable-line no-console
|
||||
|
||||
// This is intentionally a different pref-branch than the SDK-based add-on
|
||||
// used, to avoid extra weirdness for people who happen to have the SDK-based
|
||||
// installed. Though maybe we should just forcibly disable the old add-on?
|
||||
const PREF_BRANCH = "browser.newtabpage.activity-stream.";
|
||||
|
||||
const ENDPOINT_PREF = "telemetry.ping.endpoint";
|
||||
const TELEMETRY_PREF = "telemetry";
|
||||
const LOGGING_PREF = "telemetry.log";
|
||||
|
||||
/**
|
||||
* Observe various notifications and send them to a telemetry endpoint.
|
||||
*
|
||||
* @param {Object} args - optional arguments
|
||||
* @param {Function} args.prefInitHook - if present, will be called back
|
||||
* inside the Prefs constructor. Typically used from tests
|
||||
* to save off a pointer to a fake Prefs instance so that
|
||||
* stubs and spies can be inspected by the test code.
|
||||
*
|
||||
*/
|
||||
function TelemetrySender(args) {
|
||||
let prefArgs = {branch: PREF_BRANCH};
|
||||
if (args) {
|
||||
if ("prefInitHook" in args) {
|
||||
prefArgs.initHook = args.prefInitHook;
|
||||
}
|
||||
}
|
||||
|
||||
this._prefs = new Preferences(prefArgs);
|
||||
|
||||
this.enabled = this._prefs.get(TELEMETRY_PREF);
|
||||
this._onTelemetryPrefChange = this._onTelemetryPrefChange.bind(this);
|
||||
this._prefs.observe(TELEMETRY_PREF, this._onTelemetryPrefChange);
|
||||
|
||||
this.logging = this._prefs.get(LOGGING_PREF);
|
||||
this._onLoggingPrefChange = this._onLoggingPrefChange.bind(this);
|
||||
this._prefs.observe(LOGGING_PREF, this._onLoggingPrefChange);
|
||||
|
||||
this._pingEndpoint = this._prefs.get(ENDPOINT_PREF);
|
||||
}
|
||||
|
||||
TelemetrySender.prototype = {
|
||||
|
||||
_onLoggingPrefChange(prefVal) {
|
||||
this.logging = prefVal;
|
||||
},
|
||||
|
||||
_onTelemetryPrefChange(prefVal) {
|
||||
this.enabled = prefVal;
|
||||
},
|
||||
|
||||
async sendPing(data) {
|
||||
if (this.logging) {
|
||||
// performance related pings cause a lot of logging, so we mute them
|
||||
if (data.action !== "activity_stream_performance") {
|
||||
console.log(`TELEMETRY PING: ${JSON.stringify(data)}\n`); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
if (!this.enabled) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return fetch(this._pingEndpoint, {method: "POST", body: data}).then(response => {
|
||||
if (!response.ok) {
|
||||
Cu.reportError(`Ping failure with HTTP response code: ${response.status}`);
|
||||
}
|
||||
}).catch(e => {
|
||||
Cu.reportError(`Ping failure with error: ${e}`);
|
||||
});
|
||||
},
|
||||
|
||||
uninit() {
|
||||
try {
|
||||
this._prefs.ignore(TELEMETRY_PREF, this._onTelemetryPrefChange);
|
||||
this._prefs.ignore(LOGGING_PREF, this._onLoggingPrefChange);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.TelemetrySender = TelemetrySender;
|
||||
this.TelemetrySenderConstants = {
|
||||
ENDPOINT_PREF,
|
||||
TELEMETRY_PREF,
|
||||
LOGGING_PREF
|
||||
};
|
||||
this.EXPORTED_SYMBOLS = ["TelemetrySender", "TelemetrySenderConstants"];
|
|
@ -58,6 +58,10 @@ this.TopSitesFeed = class TopSitesFeed {
|
|||
this.getScreenshot(link.url);
|
||||
}
|
||||
}
|
||||
openNewWindow(action, isPrivate = false) {
|
||||
const win = action._target.browser.ownerGlobal;
|
||||
win.openLinkIn(action.data.url, "window", {private: isPrivate});
|
||||
}
|
||||
onAction(action) {
|
||||
let realRows;
|
||||
switch (action.type) {
|
||||
|
@ -73,6 +77,13 @@ this.TopSitesFeed = class TopSitesFeed {
|
|||
this.refresh(action);
|
||||
}
|
||||
break;
|
||||
case at.OPEN_NEW_WINDOW:
|
||||
this.openNewWindow(action);
|
||||
break;
|
||||
case at.OPEN_PRIVATE_WINDOW: {
|
||||
this.openNewWindow(action, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
const Joi = require("joi-browser");
|
||||
|
||||
const baseKeys = {
|
||||
client_id: Joi.string().required(),
|
||||
addon_version: Joi.string().required(),
|
||||
locale: Joi.string().required(),
|
||||
session_id: Joi.string(),
|
||||
page: Joi.valid(["about:home", "about:newtab"])
|
||||
};
|
||||
|
||||
const BasePing = Joi.object().keys(baseKeys).options({allowUnknown: true});
|
||||
|
||||
const UserEventPing = Joi.object().keys(Object.assign({}, baseKeys, {
|
||||
session_id: baseKeys.session_id.required(),
|
||||
page: baseKeys.page.required(),
|
||||
source: Joi.string().required(),
|
||||
event: Joi.string().required(),
|
||||
action: Joi.valid("activity_stream_user_event").required(),
|
||||
metadata_source: Joi.string(),
|
||||
highlight_type: Joi.valid(["bookmarks", "recommendation", "history"]),
|
||||
recommender_type: Joi.string()
|
||||
}));
|
||||
|
||||
const UndesiredPing = Joi.object().keys(Object.assign({}, baseKeys, {
|
||||
source: Joi.string().required(),
|
||||
event: Joi.string().required(),
|
||||
action: Joi.valid("activity_stream_undesired_event").required(),
|
||||
value: Joi.number().required()
|
||||
}));
|
||||
|
||||
const PerfPing = Joi.object().keys(Object.assign({}, baseKeys, {
|
||||
source: Joi.string(),
|
||||
event: Joi.string().required(),
|
||||
action: Joi.valid("activity_stream_performance_event").required(),
|
||||
value: Joi.number().required()
|
||||
}));
|
||||
|
||||
const SessionPing = Joi.object().keys(Object.assign({}, baseKeys, {
|
||||
session_id: baseKeys.session_id.required(),
|
||||
page: baseKeys.page.required(),
|
||||
session_duration: Joi.number().integer().required(),
|
||||
action: Joi.valid("activity_stream_session").required()
|
||||
}));
|
||||
|
||||
function assertMatchesSchema(ping, schema) {
|
||||
assert.isNull(Joi.validate(ping, schema).error);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
baseKeys,
|
||||
BasePing,
|
||||
UndesiredPing,
|
||||
UserEventPing,
|
||||
PerfPing,
|
||||
SessionPing,
|
||||
assertMatchesSchema
|
||||
};
|
|
@ -1,10 +1,26 @@
|
|||
const {
|
||||
actionTypes: at,
|
||||
actionCreators: ac,
|
||||
actionUtils: au,
|
||||
MAIN_MESSAGE_TYPE,
|
||||
CONTENT_MESSAGE_TYPE
|
||||
CONTENT_MESSAGE_TYPE,
|
||||
UI_CODE,
|
||||
BACKGROUND_PROCESS,
|
||||
globalImportContext
|
||||
} = require("common/Actions.jsm");
|
||||
|
||||
describe("Actions", () => {
|
||||
it("should set globalImportContext to UI_CODE", () => {
|
||||
assert.equal(globalImportContext, UI_CODE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ActionTypes", () => {
|
||||
it("should be in alpha order", () => {
|
||||
assert.equal(Object.keys(at).join(", "), Object.keys(at).sort().join(", "));
|
||||
});
|
||||
});
|
||||
|
||||
describe("ActionCreators", () => {
|
||||
describe("_RouteMessage", () => {
|
||||
it("should throw if options are not passed as the second param", () => {
|
||||
|
@ -33,6 +49,11 @@ describe("ActionCreators", () => {
|
|||
meta: {from: CONTENT_MESSAGE_TYPE, to: MAIN_MESSAGE_TYPE}
|
||||
});
|
||||
});
|
||||
it("should add the fromTarget if it was supplied", () => {
|
||||
const action = {type: "FOO", data: "BAR"};
|
||||
const newAction = ac.SendToMain(action, "port123");
|
||||
assert.equal(newAction.meta.fromTarget, "port123");
|
||||
});
|
||||
describe("isSendToMain", () => {
|
||||
it("should return true if action is SendToMain", () => {
|
||||
const newAction = ac.SendToMain({type: "FOO"});
|
||||
|
@ -90,4 +111,50 @@ describe("ActionCreators", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe("UserEvent", () => {
|
||||
it("should include the given data", () => {
|
||||
const data = {action: "foo"};
|
||||
assert.equal(ac.UserEvent(data).data, data);
|
||||
});
|
||||
it("should wrap with SendToMain", () => {
|
||||
const action = ac.UserEvent({action: "foo"});
|
||||
assert.isTrue(au.isSendToMain(action), "isSendToMain");
|
||||
});
|
||||
});
|
||||
describe("UndesiredEvent", () => {
|
||||
it("should include the given data", () => {
|
||||
const data = {action: "foo"};
|
||||
assert.equal(ac.UndesiredEvent(data).data, data);
|
||||
});
|
||||
it("should wrap with SendToMain if in UI code", () => {
|
||||
assert.isTrue(au.isSendToMain(ac.UndesiredEvent({action: "foo"})), "isSendToMain");
|
||||
});
|
||||
it("should not wrap with SendToMain if in UI code", () => {
|
||||
const action = ac.UndesiredEvent({action: "foo"}, BACKGROUND_PROCESS);
|
||||
assert.isFalse(au.isSendToMain(action), "isSendToMain");
|
||||
});
|
||||
});
|
||||
describe("PerfEvent", () => {
|
||||
it("should include the right data", () => {
|
||||
const data = {action: "foo"};
|
||||
assert.equal(ac.UndesiredEvent(data).data, data);
|
||||
});
|
||||
it("should wrap with SendToMain if in UI code", () => {
|
||||
assert.isTrue(au.isSendToMain(ac.PerfEvent({action: "foo"})), "isSendToMain");
|
||||
});
|
||||
it("should not wrap with SendToMain if in UI code", () => {
|
||||
const action = ac.PerfEvent({action: "foo"}, BACKGROUND_PROCESS);
|
||||
assert.isFalse(au.isSendToMain(action), "isSendToMain");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ActionUtils", () => {
|
||||
describe("getPortIdOfSender", () => {
|
||||
it("should return the PortID from a SendToMain action", () => {
|
||||
const portID = "foo123";
|
||||
const result = au.getPortIdOfSender(ac.SendToMain({type: "FOO"}, portID));
|
||||
assert.equal(result, portID);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,34 @@
|
|||
const {reducers, INITIAL_STATE} = require("common/Reducers.jsm");
|
||||
const {TopSites, Search} = reducers;
|
||||
const {TopSites, Search, App} = reducers;
|
||||
const {actionTypes: at} = require("common/Actions.jsm");
|
||||
|
||||
describe("Reducers", () => {
|
||||
describe("App", () => {
|
||||
it("should return the initial state", () => {
|
||||
const nextState = App(undefined, {type: "FOO"});
|
||||
assert.equal(nextState, INITIAL_STATE.App);
|
||||
});
|
||||
it("should not set initialized to true on INIT", () => {
|
||||
const nextState = App(undefined, {type: "INIT"});
|
||||
assert.propertyVal(nextState, "initialized", true);
|
||||
});
|
||||
it("should set initialized, version, and locale on INIT", () => {
|
||||
const action = {type: "INIT", data: {version: "1.2.3"}};
|
||||
const nextState = App(undefined, action);
|
||||
assert.propertyVal(nextState, "version", "1.2.3");
|
||||
});
|
||||
it("should not update state for empty action.data on LOCALE_UPDATED", () => {
|
||||
const nextState = App(undefined, {type: at.LOCALE_UPDATED});
|
||||
assert.equal(nextState, INITIAL_STATE.App);
|
||||
});
|
||||
it("should set locale, strings on LOCALE_UPDATE", () => {
|
||||
const strings = {};
|
||||
const action = {type: "LOCALE_UPDATED", data: {locale: "zh-CN", strings}};
|
||||
const nextState = App(undefined, action);
|
||||
assert.propertyVal(nextState, "locale", "zh-CN");
|
||||
assert.propertyVal(nextState, "strings", strings);
|
||||
});
|
||||
});
|
||||
describe("TopSites", () => {
|
||||
it("should return the initial state", () => {
|
||||
const nextState = TopSites(undefined, {type: "FOO"});
|
||||
|
@ -29,6 +55,58 @@ describe("Reducers", () => {
|
|||
const nextState = TopSites(oldState, action);
|
||||
assert.deepEqual(nextState, oldState);
|
||||
});
|
||||
it("should bookmark an item on PLACES_BOOKMARK_ADDED", () => {
|
||||
const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
|
||||
const action = {
|
||||
type: at.PLACES_BOOKMARK_ADDED,
|
||||
data: {
|
||||
url: "bar.com",
|
||||
bookmarkGuid: "bookmark123",
|
||||
bookmarkTitle: "Title for bar.com",
|
||||
lastModified: 1234567
|
||||
}
|
||||
};
|
||||
const nextState = TopSites(oldState, action);
|
||||
const newRow = nextState.rows[1];
|
||||
// new row has bookmark data
|
||||
assert.equal(newRow.url, action.data.url);
|
||||
assert.equal(newRow.bookmarkGuid, action.data.bookmarkGuid);
|
||||
assert.equal(newRow.bookmarkTitle, action.data.bookmarkTitle);
|
||||
assert.equal(newRow.bookmarkDateCreated, action.data.lastModified);
|
||||
|
||||
// old row is unchanged
|
||||
assert.equal(nextState.rows[0], oldState.rows[0]);
|
||||
});
|
||||
it("should remove a bookmark on PLACES_BOOKMARK_REMOVED", () => {
|
||||
const oldState = {
|
||||
rows: [{url: "foo.com"}, {
|
||||
url: "bar.com",
|
||||
bookmarkGuid: "bookmark123",
|
||||
bookmarkTitle: "Title for bar.com",
|
||||
lastModified: 123456
|
||||
}]
|
||||
};
|
||||
const action = {type: at.PLACES_BOOKMARK_REMOVED, data: {url: "bar.com"}};
|
||||
const nextState = TopSites(oldState, action);
|
||||
const newRow = nextState.rows[1];
|
||||
// new row no longer has bookmark data
|
||||
assert.equal(newRow.url, oldState.rows[1].url);
|
||||
assert.isUndefined(newRow.bookmarkGuid);
|
||||
assert.isUndefined(newRow.bookmarkTitle);
|
||||
assert.isUndefined(newRow.bookmarkDateCreated);
|
||||
|
||||
// old row is unchanged
|
||||
assert.deepEqual(nextState.rows[0], oldState.rows[0]);
|
||||
});
|
||||
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 => {
|
||||
const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
|
||||
const action = {type: event, data: {url: "bar.com"}};
|
||||
const nextState = TopSites(oldState, action);
|
||||
assert.deepEqual(nextState.rows, [{url: "foo.com"}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Search", () => {
|
||||
it("should return the initial state", () => {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
const React = require("react");
|
||||
const {shallow, mount} = require("enzyme");
|
||||
const ContextMenu = require("content-src/components/ContextMenu/ContextMenu");
|
||||
const DEFAULT_PROPS = {
|
||||
onUpdate: () => {},
|
||||
visible: false,
|
||||
options: [],
|
||||
tabbableOptionsLength: 0
|
||||
};
|
||||
|
||||
describe("<ContextMenu>", () => {
|
||||
it("shoud be hidden by default", () => {
|
||||
const wrapper = shallow(<ContextMenu {...DEFAULT_PROPS} />);
|
||||
assert.isTrue(wrapper.find(".context-menu").props().hidden);
|
||||
});
|
||||
it("should be visible if props.visible is true", () => {
|
||||
const wrapper = shallow(<ContextMenu {...DEFAULT_PROPS} visible={true} />);
|
||||
assert.isFalse(wrapper.find(".context-menu").props().hidden);
|
||||
});
|
||||
it("should render all the options provided", () => {
|
||||
const options = [{label: "item1"}, {type: "separator"}, {label: "item2"}];
|
||||
const wrapper = shallow(<ContextMenu {...DEFAULT_PROPS} options={options} />);
|
||||
assert.lengthOf(wrapper.find(".context-menu-list").children(), 3);
|
||||
});
|
||||
it("should not add a link for a separator", () => {
|
||||
const options = [{label: "item1"}, {type: "separator"}];
|
||||
const wrapper = shallow(<ContextMenu {...DEFAULT_PROPS} options={options} />);
|
||||
assert.lengthOf(wrapper.find(".separator"), 1);
|
||||
});
|
||||
it("should add a link for all types that are not separators", () => {
|
||||
const options = [{label: "item1"}, {type: "separator"}];
|
||||
const wrapper = shallow(<ContextMenu {...DEFAULT_PROPS} options={options} />);
|
||||
assert.lengthOf(wrapper.find(".context-menu-item"), 1);
|
||||
});
|
||||
it("should add an icon to items that need icons", () => {
|
||||
const options = [{label: "item1", icon: "icon1"}, {type: "separator"}];
|
||||
const wrapper = shallow(<ContextMenu {...DEFAULT_PROPS} options={options} />);
|
||||
assert.lengthOf(wrapper.find(".icon-icon1"), 1);
|
||||
});
|
||||
it("should be tabbable", () => {
|
||||
const options = [{label: "item1", icon: "icon1"}, {type: "separator"}];
|
||||
const wrapper = shallow(<ContextMenu {...DEFAULT_PROPS} options={options} />);
|
||||
assert.equal(wrapper.find(".context-menu-item").props().role, "menuitem");
|
||||
});
|
||||
it("should call onUpdate with false when an option is clicked", () => {
|
||||
const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} onUpdate={sinon.spy()} options={[{label: "item1"}]} />);
|
||||
wrapper.find(".context-menu-item").simulate("click");
|
||||
const onUpdate = wrapper.prop("onUpdate");
|
||||
assert.calledOnce(onUpdate);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
const React = require("react");
|
||||
const {shallowWithIntl} = require("test/unit/utils");
|
||||
const {_unconnected: LinkMenu} = require("content-src/components/LinkMenu/LinkMenu");
|
||||
const ContextMenu = require("content-src/components/ContextMenu/ContextMenu");
|
||||
|
||||
describe("<LinkMenu>", () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallowWithIntl(<LinkMenu site={{url: ""}} dispatch={() => {}} />);
|
||||
});
|
||||
it("should render a ContextMenu element", () => {
|
||||
assert.ok(wrapper.find(ContextMenu));
|
||||
});
|
||||
it("should pass visible, onUpdate, and options to ContextMenu", () => {
|
||||
assert.ok(wrapper.find(ContextMenu));
|
||||
const contextMenuProps = wrapper.find(ContextMenu).props();
|
||||
["visible", "onUpdate", "options"].forEach(prop => assert.property(contextMenuProps, prop));
|
||||
});
|
||||
it("should give ContextMenu the correct tabbable options length for a11y", () => {
|
||||
const options = wrapper.find(ContextMenu).props().options;
|
||||
const firstItem = options[0];
|
||||
const lastItem = options[options.length - 1];
|
||||
const middleItem = options[Math.ceil(options.length / 2)];
|
||||
|
||||
// first item should have {first: true}
|
||||
assert.isTrue(firstItem.first);
|
||||
assert.ok(!firstItem.last);
|
||||
|
||||
// last item should have {last: true}
|
||||
assert.isTrue(lastItem.last);
|
||||
assert.ok(!lastItem.first);
|
||||
|
||||
// middle items should have neither
|
||||
assert.ok(!middleItem.first);
|
||||
assert.ok(!middleItem.last);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
const React = require("react");
|
||||
const {mountWithIntl, shallowWithIntl} = require("test/unit/utils");
|
||||
const {_unconnected: Search} = require("content-src/components/Search/Search");
|
||||
const {actionTypes: at, actionUtils: au} = require("common/Actions.jsm");
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
Search: {
|
||||
currentEngine: {
|
||||
name: "google",
|
||||
icon: "google.jpg"
|
||||
}
|
||||
},
|
||||
dispatch() {}
|
||||
};
|
||||
|
||||
describe("<Search>", () => {
|
||||
it("should render a Search element", () => {
|
||||
const wrapper = shallowWithIntl(<Search {...DEFAULT_PROPS} />);
|
||||
assert.ok(wrapper.exists());
|
||||
});
|
||||
|
||||
describe("#performSearch", () => {
|
||||
function clickButtonAndGetAction(wrapper) {
|
||||
const dispatch = wrapper.prop("dispatch");
|
||||
wrapper.find(".search-button").simulate("click");
|
||||
assert.calledOnce(dispatch);
|
||||
return dispatch.firstCall.args[0];
|
||||
}
|
||||
it("should send a SendToMain action with type PERFORM_SEARCH when you click the search button", () => {
|
||||
const wrapper = mountWithIntl(<Search {...DEFAULT_PROPS} dispatch={sinon.spy()} />);
|
||||
|
||||
const action = clickButtonAndGetAction(wrapper);
|
||||
|
||||
assert.propertyVal(action, "type", at.PERFORM_SEARCH);
|
||||
assert.isTrue(au.isSendToMain(action));
|
||||
});
|
||||
it("should send an action with the right engineName ", () => {
|
||||
const props = {Search: {currentEngine: {name: "foo123"}}, dispatch: sinon.spy()};
|
||||
const wrapper = mountWithIntl(<Search {...props} />);
|
||||
|
||||
const action = clickButtonAndGetAction(wrapper);
|
||||
|
||||
assert.propertyVal(action.data, "engineName", "foo123");
|
||||
});
|
||||
it("should send an action with the right searchString ", () => {
|
||||
const wrapper = mountWithIntl(<Search {...DEFAULT_PROPS} dispatch={sinon.spy()} />);
|
||||
wrapper.setState({searchString: "hello123"});
|
||||
|
||||
const action = clickButtonAndGetAction(wrapper);
|
||||
|
||||
assert.propertyVal(action.data, "searchString", "hello123");
|
||||
});
|
||||
});
|
||||
|
||||
it("should update state.searchString on a change event", () => {
|
||||
const wrapper = mountWithIntl(<Search {...DEFAULT_PROPS} />);
|
||||
const inputEl = wrapper.find("#search-input");
|
||||
// as the value in the input field changes, it will update the search string
|
||||
inputEl.simulate("change", {target: {value: "hello"}});
|
||||
|
||||
assert.equal(wrapper.state().searchString, "hello");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
const React = require("react");
|
||||
const {shallow} = require("enzyme");
|
||||
const {_unconnected: TopSites, TopSite} = require("content-src/components/TopSites/TopSites");
|
||||
const LinkMenu = require("content-src/components/LinkMenu/LinkMenu");
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
TopSites: {rows: []},
|
||||
dispatch() {}
|
||||
};
|
||||
|
||||
describe("<TopSites>", () => {
|
||||
it("should render a TopSites element", () => {
|
||||
const wrapper = shallow(<TopSites {...DEFAULT_PROPS} />);
|
||||
assert.ok(wrapper.exists());
|
||||
});
|
||||
it("should render a TopSite for each link with the right url", () => {
|
||||
const rows = [{url: "https://foo.com"}, {url: "https://bar.com"}];
|
||||
|
||||
const wrapper = shallow(<TopSites {...DEFAULT_PROPS} TopSites={{rows}} />);
|
||||
|
||||
const links = wrapper.find(TopSite);
|
||||
assert.lengthOf(links, 2);
|
||||
links.forEach((link, i) => assert.equal(link.props().link.url, rows[i].url));
|
||||
});
|
||||
});
|
||||
|
||||
describe("<TopSite>", () => {
|
||||
let link;
|
||||
beforeEach(() => {
|
||||
link = {url: "https://foo.com", screenshot: "foo.jpg"};
|
||||
});
|
||||
|
||||
it("should render a TopSite", () => {
|
||||
const wrapper = shallow(<TopSite link={link} />);
|
||||
assert.ok(wrapper.exists());
|
||||
});
|
||||
it("should add the right url", () => {
|
||||
link.url = "https://www.foobar.org";
|
||||
const wrapper = shallow(<TopSite link={link} />);
|
||||
assert.propertyVal(wrapper.find("a").props(), "href", "https://www.foobar.org");
|
||||
});
|
||||
it("should render a shortened title based off the url", () => {
|
||||
link.url = "https://www.foobar.org";
|
||||
link.eTLD = "org";
|
||||
const wrapper = shallow(<TopSite link={link} />);
|
||||
const titleEl = wrapper.find(".title");
|
||||
|
||||
assert.equal(titleEl.text(), "foobar");
|
||||
});
|
||||
it("should render the first letter of the title as a fallback for missing screenshots", () => {
|
||||
const wrapper = shallow(<TopSite link={link} />);
|
||||
assert.equal(wrapper.find(".letter-fallback").text(), "f");
|
||||
});
|
||||
it("should render a screenshot with the .active class, if it is provided", () => {
|
||||
const wrapper = shallow(<TopSite link={link} />);
|
||||
const screenshotEl = wrapper.find(".screenshot");
|
||||
|
||||
assert.propertyVal(screenshotEl.props().style, "backgroundImage", "url(foo.jpg)");
|
||||
assert.isTrue(screenshotEl.hasClass("active"));
|
||||
});
|
||||
it("should not add the .active class to the screenshot element if no screenshot prop is provided", () => {
|
||||
link.screenshot = null;
|
||||
const wrapper = shallow(<TopSite link={link} />);
|
||||
assert.isFalse(wrapper.find(".screenshot").hasClass("active"));
|
||||
});
|
||||
it("should have .active class, on top-site-outer if context menu is open", () => {
|
||||
const wrapper = shallow(<TopSite link={link} index={1} />);
|
||||
wrapper.setState({showContextMenu: true, activeTile: 1});
|
||||
const topSiteEl = wrapper.find(".top-site-outer");
|
||||
assert.isTrue(topSiteEl.hasClass("active"));
|
||||
});
|
||||
it("should not add .active class, on top-site-outer if context menu is closed", () => {
|
||||
const wrapper = shallow(<TopSite link={link} index={1} />);
|
||||
wrapper.setState({showContextMenu: false, activeTile: 1});
|
||||
const topSiteEl = wrapper.find(".top-site-outer");
|
||||
assert.isFalse(topSiteEl.hasClass("active"));
|
||||
});
|
||||
it("should render a context menu button", () => {
|
||||
const wrapper = shallow(<TopSite link={link} />);
|
||||
assert.ok(wrapper.find(".context-menu-button"));
|
||||
});
|
||||
it("should render a link menu when button is clicked", () => {
|
||||
const wrapper = shallow(<TopSite link={link} />);
|
||||
let button = wrapper.find(".context-menu-button");
|
||||
button.simulate("click", {preventDefault: () => {}});
|
||||
assert.isTrue(wrapper.find(LinkMenu).props().visible);
|
||||
});
|
||||
it("should not render a link menu by default", () => {
|
||||
const wrapper = shallow(<TopSite link={link} />);
|
||||
assert.isFalse(wrapper.find(LinkMenu).props().visible);
|
||||
});
|
||||
it("should pass visible, onUpdate, site, and index to LinkMenu", () => {
|
||||
const wrapper = shallow(<TopSite link={link} />);
|
||||
const linkMenuProps = wrapper.find(LinkMenu).props();
|
||||
["visible", "onUpdate", "site", "index"].forEach(prop => assert.property(linkMenuProps, prop));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
const DetectUserSessionStart = require("content-src/lib/detect-user-session-start");
|
||||
const {actionTypes: at} = require("common/Actions.jsm");
|
||||
|
||||
describe("detectUserSessionStart", () => {
|
||||
describe("#sendEventOrAddListener", () => {
|
||||
it("should call ._sendEvent immediately if the document is visible", () => {
|
||||
const mockDocument = {visibilityState: "visible"};
|
||||
const instance = new DetectUserSessionStart({document: mockDocument});
|
||||
sinon.stub(instance, "_sendEvent");
|
||||
|
||||
instance.sendEventOrAddListener();
|
||||
|
||||
assert.calledOnce(instance._sendEvent);
|
||||
});
|
||||
it("should add an event listener on visibility changes the document is not visible", () => {
|
||||
const mockDocument = {visibilityState: "hidden", addEventListener: sinon.spy()};
|
||||
const instance = new DetectUserSessionStart({document: mockDocument});
|
||||
sinon.stub(instance, "_sendEvent");
|
||||
|
||||
instance.sendEventOrAddListener();
|
||||
|
||||
assert.notCalled(instance._sendEvent);
|
||||
assert.calledWith(mockDocument.addEventListener, "visibilitychange", instance._onVisibilityChange);
|
||||
});
|
||||
});
|
||||
describe("#_sendEvent", () => {
|
||||
it("should send an async message with the NEW_TAB_VISIBLE event", () => {
|
||||
const sendAsyncMessage = sinon.spy();
|
||||
const instance = new DetectUserSessionStart({sendAsyncMessage});
|
||||
|
||||
instance._sendEvent();
|
||||
|
||||
assert.calledWith(sendAsyncMessage, "ActivityStream:ContentToMain", {type: at.NEW_TAB_VISIBLE});
|
||||
});
|
||||
});
|
||||
describe("_onVisibilityChange", () => {
|
||||
it("should not send an event if visiblity is not visible", () => {
|
||||
const instance = new DetectUserSessionStart({document: {visibilityState: "hidden"}});
|
||||
sinon.stub(instance, "_sendEvent");
|
||||
|
||||
instance._onVisibilityChange();
|
||||
|
||||
assert.notCalled(instance._sendEvent);
|
||||
});
|
||||
it("should send an event and remove the event listener if visibility is visible", () => {
|
||||
const mockDocument = {visibilityState: "visible", removeEventListener: sinon.spy()};
|
||||
const instance = new DetectUserSessionStart({document: mockDocument});
|
||||
sinon.stub(instance, "_sendEvent");
|
||||
|
||||
instance._onVisibilityChange();
|
||||
|
||||
assert.calledOnce(instance._sendEvent);
|
||||
assert.calledWith(mockDocument.removeEventListener, "visibilitychange", instance._onVisibilityChange);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
const shortURL = require("content-src/lib/short-url");
|
||||
|
||||
describe("shortURL", () => {
|
||||
it("should return a blank string if url and hostname is falsey", () => {
|
||||
assert.equal(shortURL({url: ""}), "");
|
||||
assert.equal(shortURL({hostname: null}), "");
|
||||
});
|
||||
|
||||
it("should remove the eTLD, if provided", () => {
|
||||
assert.equal(shortURL({hostname: "com.blah.com", eTLD: "com"}), "com.blah");
|
||||
});
|
||||
|
||||
it("should use the hostname, if provided", () => {
|
||||
assert.equal(shortURL({hostname: "foo.com", url: "http://bar.com", eTLD: "com"}), "foo");
|
||||
});
|
||||
|
||||
it("should get the hostname from .url if necessary", () => {
|
||||
assert.equal(shortURL({url: "http://bar.com", eTLD: "com"}), "bar");
|
||||
});
|
||||
|
||||
it("should not strip out www if not first subdomain", () => {
|
||||
assert.equal(shortURL({hostname: "foo.www.com", eTLD: "com"}), "foo.www");
|
||||
});
|
||||
|
||||
it("should convert to lowercase", () => {
|
||||
assert.equal(shortURL({url: "HTTP://FOO.COM", eTLD: "com"}), "foo");
|
||||
});
|
||||
});
|
|
@ -4,15 +4,16 @@ describe("ActivityStream", () => {
|
|||
let sandbox;
|
||||
let as;
|
||||
let ActivityStream;
|
||||
function NewTabInit() {}
|
||||
function TopSitesFeed() {}
|
||||
function SearchFeed() {}
|
||||
function Fake() {}
|
||||
before(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
({ActivityStream} = injector({
|
||||
"lib/NewTabInit.jsm": {NewTabInit},
|
||||
"lib/TopSitesFeed.jsm": {TopSitesFeed},
|
||||
"lib/SearchFeed.jsm": {SearchFeed}
|
||||
"lib/LocalizationFeed.jsm": {LocalizationFeed: Fake},
|
||||
"lib/NewTabInit.jsm": {NewTabInit: Fake},
|
||||
"lib/PlacesFeed.jsm": {PlacesFeed: Fake},
|
||||
"lib/SearchFeed.jsm": {SearchFeed: Fake},
|
||||
"lib/TelemetryFeed.jsm": {TelemetryFeed: Fake},
|
||||
"lib/TopSitesFeed.jsm": {TopSitesFeed: Fake}
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -40,6 +41,17 @@ describe("ActivityStream", () => {
|
|||
it("should call .store.init", () => {
|
||||
assert.calledOnce(as.store.init);
|
||||
});
|
||||
it("should emit an INIT event with the right version", () => {
|
||||
as = new ActivityStream({version: "1.2.3"});
|
||||
sandbox.stub(as.store, "init");
|
||||
sandbox.stub(as.store, "dispatch");
|
||||
|
||||
as.init();
|
||||
|
||||
assert.calledOnce(as.store.dispatch);
|
||||
const action = as.store.dispatch.firstCall.args[0];
|
||||
assert.propertyVal(action.data, "version", "1.2.3");
|
||||
});
|
||||
});
|
||||
describe("#uninit", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -54,17 +66,29 @@ describe("ActivityStream", () => {
|
|||
});
|
||||
});
|
||||
describe("feeds", () => {
|
||||
it("should create a Localization feed", () => {
|
||||
const feed = as.feeds["feeds.localization"]();
|
||||
assert.instanceOf(feed, Fake);
|
||||
});
|
||||
it("should create a NewTabInit feed", () => {
|
||||
const feed = as.feeds["feeds.newtabinit"]();
|
||||
assert.instanceOf(feed, NewTabInit);
|
||||
assert.instanceOf(feed, Fake);
|
||||
});
|
||||
it("should create a Places feed", () => {
|
||||
const feed = as.feeds["feeds.places"]();
|
||||
assert.instanceOf(feed, Fake);
|
||||
});
|
||||
it("should create a TopSites feed", () => {
|
||||
const feed = as.feeds["feeds.topsites"]();
|
||||
assert.instanceOf(feed, TopSitesFeed);
|
||||
assert.instanceOf(feed, Fake);
|
||||
});
|
||||
it("should create a Telemetry feed", () => {
|
||||
const feed = as.feeds["feeds.telemetry"]();
|
||||
assert.instanceOf(feed, Fake);
|
||||
});
|
||||
it("should create a Search feed", () => {
|
||||
const feed = as.feeds["feeds.search"]();
|
||||
assert.instanceOf(feed, SearchFeed);
|
||||
assert.instanceOf(feed, Fake);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
"use strict";
|
||||
const {LocalizationFeed} = require("lib/LocalizationFeed.jsm");
|
||||
const {GlobalOverrider} = require("test/unit/utils");
|
||||
const {actionTypes: at} = require("common/Actions.jsm");
|
||||
|
||||
const DEFAULT_LOCALE = "en-US";
|
||||
const TEST_STRINGS = {
|
||||
[DEFAULT_LOCALE]: {
|
||||
foo: "Foo",
|
||||
too: "Too"
|
||||
},
|
||||
"it": {
|
||||
foo: "Bar",
|
||||
too: "Boo"
|
||||
},
|
||||
"ru": {foo: "Baz"}
|
||||
};
|
||||
|
||||
describe("Localization Feed", () => {
|
||||
let feed;
|
||||
let globals;
|
||||
before(() => {
|
||||
globals = new GlobalOverrider();
|
||||
});
|
||||
beforeEach(() => {
|
||||
feed = new LocalizationFeed();
|
||||
feed.store = {dispatch: sinon.spy()};
|
||||
});
|
||||
afterEach(() => {
|
||||
globals.restore();
|
||||
});
|
||||
|
||||
it("should fetch strings on init", async () => {
|
||||
sinon.stub(feed, "updateLocale");
|
||||
fetch.returns(Promise.resolve({json() { return Promise.resolve(TEST_STRINGS); }}));
|
||||
|
||||
await feed.init();
|
||||
|
||||
assert.deepEqual(feed.allStrings, TEST_STRINGS);
|
||||
assert.calledOnce(feed.updateLocale);
|
||||
});
|
||||
|
||||
describe("#updateLocale", () => {
|
||||
beforeEach(() => {
|
||||
feed.allStrings = TEST_STRINGS;
|
||||
});
|
||||
|
||||
it("should dispatch with locale and strings for default", () => {
|
||||
const locale = DEFAULT_LOCALE;
|
||||
feed.updateLocale();
|
||||
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
const arg = feed.store.dispatch.firstCall.args[0];
|
||||
assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
|
||||
assert.propertyVal(arg.data, "locale", locale);
|
||||
assert.deepEqual(arg.data.strings, TEST_STRINGS[locale]);
|
||||
});
|
||||
it("should use strings for other locale", () => {
|
||||
const locale = "it";
|
||||
global.Services.locale.getRequestedLocale.returns(locale);
|
||||
|
||||
feed.updateLocale();
|
||||
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
const arg = feed.store.dispatch.firstCall.args[0];
|
||||
assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
|
||||
assert.propertyVal(arg.data, "locale", locale);
|
||||
assert.deepEqual(arg.data.strings, TEST_STRINGS[locale]);
|
||||
});
|
||||
it("should use some fallback strings for partial locale", () => {
|
||||
const locale = "ru";
|
||||
global.Services.locale.getRequestedLocale.returns(locale);
|
||||
|
||||
feed.updateLocale();
|
||||
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
const arg = feed.store.dispatch.firstCall.args[0];
|
||||
assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
|
||||
assert.propertyVal(arg.data, "locale", locale);
|
||||
assert.deepEqual(arg.data.strings, {
|
||||
foo: TEST_STRINGS[locale].foo,
|
||||
too: TEST_STRINGS[DEFAULT_LOCALE].too
|
||||
});
|
||||
});
|
||||
it("should use all default strings for unknown locale", () => {
|
||||
const locale = "xyz";
|
||||
global.Services.locale.getRequestedLocale.returns(locale);
|
||||
|
||||
feed.updateLocale();
|
||||
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
const arg = feed.store.dispatch.firstCall.args[0];
|
||||
assert.propertyVal(arg, "type", at.LOCALE_UPDATED);
|
||||
assert.propertyVal(arg.data, "locale", locale);
|
||||
assert.deepEqual(arg.data.strings, TEST_STRINGS[DEFAULT_LOCALE]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#observe", () => {
|
||||
it("should update locale on locale change event", () => {
|
||||
sinon.stub(feed, "updateLocale");
|
||||
|
||||
feed.observe(null, "intl:requested-locales-changed");
|
||||
|
||||
assert.calledOnce(feed.updateLocale);
|
||||
});
|
||||
it("shouldn't update locale on other event", () => {
|
||||
sinon.stub(feed, "updateLocale");
|
||||
|
||||
feed.observe(null, "some-other-notification");
|
||||
|
||||
assert.notCalled(feed.updateLocale);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onAction", () => {
|
||||
it("should addObserver on INIT", () => {
|
||||
feed.onAction({type: at.INIT});
|
||||
|
||||
assert.calledOnce(global.Services.obs.addObserver);
|
||||
});
|
||||
it("should removeObserver on UNINIT", () => {
|
||||
feed.onAction({type: at.UNINIT});
|
||||
|
||||
assert.calledOnce(global.Services.obs.removeObserver);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,208 @@
|
|||
const {PlacesFeed} = require("lib/PlacesFeed.jsm");
|
||||
const {HistoryObserver, BookmarksObserver} = PlacesFeed;
|
||||
const {GlobalOverrider} = require("test/unit/utils");
|
||||
const {actionTypes: at} = require("common/Actions.jsm");
|
||||
|
||||
const FAKE_BOOKMARK = {bookmarkGuid: "xi31", bookmarkTitle: "Foo", lastModified: 123214232, url: "foo.com"};
|
||||
const TYPE_BOOKMARK = 0; // This is fake, for testing
|
||||
|
||||
const BLOCKED_EVENT = "newtab-linkBlocked"; // The event dispatched in NewTabUtils when a link is blocked;
|
||||
|
||||
describe("PlacesFeed", () => {
|
||||
let globals;
|
||||
let sandbox;
|
||||
let feed;
|
||||
beforeEach(() => {
|
||||
globals = new GlobalOverrider();
|
||||
sandbox = globals.sandbox;
|
||||
globals.set("NewTabUtils", {
|
||||
activityStreamProvider: {getBookmark() {}},
|
||||
activityStreamLinks: {
|
||||
addBookmark: sandbox.spy(),
|
||||
deleteBookmark: sandbox.spy(),
|
||||
deleteHistoryEntry: sandbox.spy(),
|
||||
blockURL: sandbox.spy()
|
||||
}
|
||||
});
|
||||
globals.set("PlacesUtils", {
|
||||
history: {addObserver: sandbox.spy(), removeObserver: sandbox.spy()},
|
||||
bookmarks: {TYPE_BOOKMARK, addObserver: sandbox.spy(), removeObserver: sandbox.spy()}
|
||||
});
|
||||
|
||||
feed = new PlacesFeed();
|
||||
feed.store = {dispatch: sinon.spy()};
|
||||
});
|
||||
afterEach(() => globals.restore());
|
||||
|
||||
it("should have a HistoryObserver that dispatches to the store", () => {
|
||||
assert.instanceOf(feed.historyObserver, HistoryObserver);
|
||||
const action = {type: "FOO"};
|
||||
|
||||
feed.historyObserver.dispatch(action);
|
||||
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);
|
||||
});
|
||||
|
||||
it("should have a BookmarksObserver that dispatch to the store", () => {
|
||||
assert.instanceOf(feed.bookmarksObserver, BookmarksObserver);
|
||||
const action = {type: "FOO"};
|
||||
|
||||
feed.bookmarksObserver.dispatch(action);
|
||||
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);
|
||||
});
|
||||
|
||||
describe("#onAction", () => {
|
||||
it("should add bookmark, history, blocked observers on INIT", () => {
|
||||
feed.onAction({type: at.INIT});
|
||||
|
||||
assert.calledWith(global.PlacesUtils.history.addObserver, feed.historyObserver, true);
|
||||
assert.calledWith(global.PlacesUtils.bookmarks.addObserver, feed.bookmarksObserver, true);
|
||||
assert.calledWith(global.Services.obs.addObserver, feed, BLOCKED_EVENT);
|
||||
});
|
||||
it("should remove bookmark, history, blocked observers on UNINIT", () => {
|
||||
feed.onAction({type: at.UNINIT});
|
||||
|
||||
assert.calledWith(global.PlacesUtils.history.removeObserver, feed.historyObserver);
|
||||
assert.calledWith(global.PlacesUtils.bookmarks.removeObserver, feed.bookmarksObserver);
|
||||
assert.calledWith(global.Services.obs.removeObserver, feed, BLOCKED_EVENT);
|
||||
});
|
||||
it("should block a url on BLOCK_URL", () => {
|
||||
feed.onAction({type: at.BLOCK_URL, data: "apple.com"});
|
||||
assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, {url: "apple.com"});
|
||||
});
|
||||
it("should bookmark a url on BOOKMARK_URL", () => {
|
||||
feed.onAction({type: at.BOOKMARK_URL, data: "pear.com"});
|
||||
assert.calledWith(global.NewTabUtils.activityStreamLinks.addBookmark, "pear.com");
|
||||
});
|
||||
it("should delete a bookmark on DELETE_BOOKMARK_BY_ID", () => {
|
||||
feed.onAction({type: at.DELETE_BOOKMARK_BY_ID, data: "g123kd"});
|
||||
assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteBookmark, "g123kd");
|
||||
});
|
||||
it("should delete a history entry on DELETE_HISTORY_URL", () => {
|
||||
feed.onAction({type: at.DELETE_HISTORY_URL, data: "guava.com"});
|
||||
assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteHistoryEntry, "guava.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#observe", () => {
|
||||
it("should dispatch a PLACES_LINK_BLOCKED action with the url of the blocked link", () => {
|
||||
feed.observe(null, BLOCKED_EVENT, "foo123.com");
|
||||
assert.equal(feed.store.dispatch.firstCall.args[0].type, at.PLACES_LINK_BLOCKED);
|
||||
assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {url: "foo123.com"});
|
||||
});
|
||||
it("should not call dispatch if the topic is something other than BLOCKED_EVENT", () => {
|
||||
feed.observe(null, "someotherevent");
|
||||
assert.notCalled(feed.store.dispatch);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HistoryObserver", () => {
|
||||
let dispatch;
|
||||
let observer;
|
||||
beforeEach(() => {
|
||||
dispatch = sandbox.spy();
|
||||
observer = new HistoryObserver(dispatch);
|
||||
});
|
||||
it("should have a QueryInterface property", () => {
|
||||
assert.property(observer, "QueryInterface");
|
||||
});
|
||||
describe("#onDeleteURI", () => {
|
||||
it("should dispatch a PLACES_LINK_DELETED action with the right url", () => {
|
||||
observer.onDeleteURI({spec: "foo.com"});
|
||||
assert.calledWith(dispatch, {type: at.PLACES_LINK_DELETED, data: {url: "foo.com"}});
|
||||
});
|
||||
});
|
||||
describe("#onClearHistory", () => {
|
||||
it("should dispatch a PLACES_HISTORY_CLEARED action", () => {
|
||||
observer.onClearHistory();
|
||||
assert.calledWith(dispatch, {type: at.PLACES_HISTORY_CLEARED});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("BookmarksObserver", () => {
|
||||
let dispatch;
|
||||
let observer;
|
||||
beforeEach(() => {
|
||||
dispatch = sandbox.spy();
|
||||
observer = new BookmarksObserver(dispatch);
|
||||
});
|
||||
it("should have a QueryInterface property", () => {
|
||||
assert.property(observer, "QueryInterface");
|
||||
});
|
||||
describe("#onItemAdded", () => {
|
||||
beforeEach(() => {
|
||||
// Make sure getBookmark returns our fake bookmark if it is called with the expected guid
|
||||
sandbox.stub(global.NewTabUtils.activityStreamProvider, "getBookmark")
|
||||
.withArgs(FAKE_BOOKMARK.guid).returns(Promise.resolve(FAKE_BOOKMARK));
|
||||
});
|
||||
it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data", async () => {
|
||||
// Yes, onItemAdded has at least 8 arguments. See function definition for docs.
|
||||
const args = [null, null, null, TYPE_BOOKMARK, null, null, null, FAKE_BOOKMARK.guid];
|
||||
await observer.onItemAdded(...args);
|
||||
|
||||
assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_ADDED, data: FAKE_BOOKMARK});
|
||||
});
|
||||
it("should catch errors gracefully", async () => {
|
||||
const e = new Error("test error");
|
||||
global.NewTabUtils.activityStreamProvider.getBookmark.restore();
|
||||
sandbox.stub(global.NewTabUtils.activityStreamProvider, "getBookmark")
|
||||
.returns(Promise.reject(e));
|
||||
|
||||
const args = [null, null, null, TYPE_BOOKMARK, null, null, null, FAKE_BOOKMARK.guid];
|
||||
await observer.onItemAdded(...args);
|
||||
|
||||
assert.calledWith(global.Components.utils.reportError, e);
|
||||
});
|
||||
it("should ignore events that are not of TYPE_BOOKMARK", async () => {
|
||||
const args = [null, null, null, "nottypebookmark"];
|
||||
await observer.onItemAdded(...args);
|
||||
assert.notCalled(dispatch);
|
||||
});
|
||||
});
|
||||
describe("#onItemRemoved", () => {
|
||||
it("should ignore events that are not of TYPE_BOOKMARK", async () => {
|
||||
await observer.onItemRemoved(null, null, null, "nottypebookmark", null, "123foo");
|
||||
assert.notCalled(dispatch);
|
||||
});
|
||||
it("should dispatch a PLACES_BOOKMARK_REMOVED action with the right URL and bookmarkGuid", () => {
|
||||
observer.onItemRemoved(null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo");
|
||||
assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_REMOVED, data: {bookmarkGuid: "123foo", url: "foo.com"}});
|
||||
});
|
||||
});
|
||||
describe("#onItemChanged", () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(global.NewTabUtils.activityStreamProvider, "getBookmark")
|
||||
.withArgs(FAKE_BOOKMARK.guid).returns(Promise.resolve(FAKE_BOOKMARK));
|
||||
});
|
||||
it("should dispatch a PLACES_BOOKMARK_CHANGED action with the bookmark data", async () => {
|
||||
const args = [null, "title", null, null, null, TYPE_BOOKMARK, null, FAKE_BOOKMARK.guid];
|
||||
await observer.onItemChanged(...args);
|
||||
|
||||
assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_CHANGED, data: FAKE_BOOKMARK});
|
||||
});
|
||||
it("should catch errors gracefully", async () => {
|
||||
const e = new Error("test error");
|
||||
global.NewTabUtils.activityStreamProvider.getBookmark.restore();
|
||||
sandbox.stub(global.NewTabUtils.activityStreamProvider, "getBookmark")
|
||||
.returns(Promise.reject(e));
|
||||
|
||||
const args = [null, "title", null, null, null, TYPE_BOOKMARK, null, FAKE_BOOKMARK.guid];
|
||||
await observer.onItemChanged(...args);
|
||||
|
||||
assert.calledWith(global.Components.utils.reportError, e);
|
||||
});
|
||||
it("should ignore events that are not of TYPE_BOOKMARK", async () => {
|
||||
await observer.onItemChanged(null, "title", null, null, null, "nottypebookmark");
|
||||
assert.notCalled(dispatch);
|
||||
});
|
||||
it("should ignore events that are not changes to uri/title", async () => {
|
||||
await observer.onItemChanged(null, "tags", null, null, null, TYPE_BOOKMARK);
|
||||
assert.notCalled(dispatch);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -20,8 +20,9 @@ describe("Search Feed", () => {
|
|||
afterEach(() => globals.reset());
|
||||
after(() => globals.restore());
|
||||
|
||||
it("should call get state (with true) from the content search provider on INIT", () => {
|
||||
feed.onAction({type: at.INIT});
|
||||
it("should call get state (with true) from the content search provider on INIT", async() => {
|
||||
await feed.onAction({type: at.INIT});
|
||||
|
||||
// calling currentStateObj with 'true' allows us to return a data uri for the
|
||||
// icon, instead of an array buffer
|
||||
assert.calledWith(global.ContentSearch.currentStateObj, true);
|
||||
|
@ -41,19 +42,25 @@ describe("Search Feed", () => {
|
|||
feed.onAction({type: at.UNINIT});
|
||||
assert.calledOnce(feed.removeObservers);
|
||||
});
|
||||
it("should call services.obs.addObserver on INIT", () => {
|
||||
it("should add event handlers on INIT", () => {
|
||||
feed.onAction({type: at.INIT});
|
||||
|
||||
assert.calledOnce(global.Services.obs.addObserver);
|
||||
assert.calledOnce(global.Services.mm.addMessageListener);
|
||||
});
|
||||
it("should call services.obs.removeObserver on UNINIT", () => {
|
||||
it("should remove event handlers on UNINIT", () => {
|
||||
feed.onAction({type: at.UNINIT});
|
||||
|
||||
assert.calledOnce(global.Services.obs.removeObserver);
|
||||
assert.calledOnce(global.Services.mm.removeMessageListener);
|
||||
});
|
||||
it("should dispatch one event with the state", async() => {
|
||||
feed.contentSearch = Promise.resolve(global.ContentSearch);
|
||||
|
||||
await feed.getState();
|
||||
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
});
|
||||
it("should dispatch one event with the state", () => (
|
||||
feed.getState().then(() => {
|
||||
assert.calledOnce(feed.store.dispatch);
|
||||
})
|
||||
));
|
||||
it("should perform a search on PERFORM_SEARCH", () => {
|
||||
sinon.stub(feed, "performSearch");
|
||||
feed.onAction({_target: {browser: {}}, type: at.PERFORM_SEARCH});
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
const injector = require("inject!lib/TelemetryFeed.jsm");
|
||||
const {GlobalOverrider} = require("test/unit/utils");
|
||||
const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
|
||||
const {
|
||||
BasePing,
|
||||
UndesiredPing,
|
||||
UserEventPing,
|
||||
PerfPing,
|
||||
SessionPing,
|
||||
assertMatchesSchema
|
||||
} = require("test/schemas/pings");
|
||||
|
||||
const FAKE_TELEMETRY_ID = "foo123";
|
||||
const FAKE_UUID = "{foo-123-foo}";
|
||||
|
||||
describe("TelemetryFeed", () => {
|
||||
let globals;
|
||||
let sandbox;
|
||||
let store = {getState() { return {App: {version: "1.0.0", locale: "en-US"}}; }};
|
||||
let instance;
|
||||
class TelemetrySender {sendPing() {} uninit() {}}
|
||||
const {TelemetryFeed} = injector({"lib/TelemetrySender.jsm": {TelemetrySender}});
|
||||
|
||||
function addSession(id) {
|
||||
instance.addSession(id);
|
||||
return instance.sessions.get(id);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
globals = new GlobalOverrider();
|
||||
sandbox = globals.sandbox;
|
||||
globals.set("ClientID", {getClientID: sandbox.spy(async () => FAKE_TELEMETRY_ID)});
|
||||
globals.set("gUUIDGenerator", {generateUUID: () => FAKE_UUID});
|
||||
instance = new TelemetryFeed();
|
||||
instance.store = store;
|
||||
});
|
||||
afterEach(() => {
|
||||
globals.restore();
|
||||
});
|
||||
describe("#init", () => {
|
||||
it("should add .telemetrySender, a TelemetrySender instance", async () => {
|
||||
assert.isNull(instance.telemetrySender);
|
||||
await instance.init();
|
||||
assert.instanceOf(instance.telemetrySender, TelemetrySender);
|
||||
});
|
||||
it("should add .telemetryClientId from the ClientID module", async () => {
|
||||
assert.isNull(instance.telemetryClientId);
|
||||
await instance.init();
|
||||
assert.equal(instance.telemetryClientId, FAKE_TELEMETRY_ID);
|
||||
});
|
||||
});
|
||||
describe("#addSession", () => {
|
||||
it("should add a session", () => {
|
||||
addSession("foo");
|
||||
assert.isTrue(instance.sessions.has("foo"));
|
||||
});
|
||||
it("should set the start_time", () => {
|
||||
sandbox.spy(Components.utils, "now");
|
||||
const session = addSession("foo");
|
||||
assert.calledOnce(Components.utils.now);
|
||||
assert.equal(session.start_time, Components.utils.now.firstCall.returnValue);
|
||||
});
|
||||
it("should set the session_id", () => {
|
||||
sandbox.spy(global.gUUIDGenerator, "generateUUID");
|
||||
const session = addSession("foo");
|
||||
assert.calledOnce(global.gUUIDGenerator.generateUUID);
|
||||
assert.equal(session.session_id, global.gUUIDGenerator.generateUUID.firstCall.returnValue);
|
||||
});
|
||||
it("should set the page", () => {
|
||||
const session = addSession("foo");
|
||||
assert.equal(session.page, "about:newtab"); // This is hardcoded for now.
|
||||
});
|
||||
});
|
||||
describe("#endSession", () => {
|
||||
it("should not throw if there is no session for the given port ID", () => {
|
||||
assert.doesNotThrow(() => instance.endSession("doesn't exist"));
|
||||
});
|
||||
it("should add a session_duration", () => {
|
||||
sandbox.stub(instance, "sendEvent");
|
||||
const session = addSession("foo");
|
||||
instance.endSession("foo");
|
||||
assert.property(session, "session_duration");
|
||||
});
|
||||
it("should remove the session from .sessions", () => {
|
||||
sandbox.stub(instance, "sendEvent");
|
||||
addSession("foo");
|
||||
instance.endSession("foo");
|
||||
assert.isFalse(instance.sessions.has("foo"));
|
||||
});
|
||||
it("should call createSessionSendEvent and sendEvent with the sesssion", () => {
|
||||
sandbox.stub(instance, "sendEvent");
|
||||
sandbox.stub(instance, "createSessionEndEvent");
|
||||
const session = addSession("foo");
|
||||
instance.endSession("foo");
|
||||
|
||||
// Did we call sendEvent with the result of createSessionEndEvent?
|
||||
assert.calledWith(instance.createSessionEndEvent, session);
|
||||
assert.calledWith(instance.sendEvent, instance.createSessionEndEvent.firstCall.returnValue);
|
||||
});
|
||||
});
|
||||
describe("ping creators", () => {
|
||||
beforeEach(async () => await instance.init());
|
||||
describe("#createPing", () => {
|
||||
it("should create a valid base ping without a session if no portID is supplied", () => {
|
||||
const ping = instance.createPing();
|
||||
assertMatchesSchema(ping, BasePing);
|
||||
assert.notProperty(ping, "session_id");
|
||||
});
|
||||
it("should create a valid base ping with session info if a portID is supplied", () => {
|
||||
// Add a session
|
||||
const portID = "foo";
|
||||
instance.addSession(portID);
|
||||
const sessionID = instance.sessions.get(portID).session_id;
|
||||
|
||||
// Create a ping referencing the session
|
||||
const ping = instance.createPing(portID);
|
||||
assertMatchesSchema(ping, BasePing);
|
||||
|
||||
// Make sure we added the right session-related stuff to the ping
|
||||
assert.propertyVal(ping, "session_id", sessionID);
|
||||
assert.propertyVal(ping, "page", "about:newtab");
|
||||
});
|
||||
});
|
||||
describe("#createUserEvent", () => {
|
||||
it("should create a valid event", () => {
|
||||
const portID = "foo";
|
||||
const data = {source: "TOP_SITES", event: "CLICK"};
|
||||
const action = ac.SendToMain(ac.UserEvent(data), portID);
|
||||
const session = addSession(portID);
|
||||
const ping = instance.createUserEvent(action);
|
||||
|
||||
// Is it valid?
|
||||
assertMatchesSchema(ping, UserEventPing);
|
||||
// Does it have the right session_id?
|
||||
assert.propertyVal(ping, "session_id", session.session_id);
|
||||
});
|
||||
});
|
||||
describe("#createUndesiredEvent", () => {
|
||||
it("should create a valid event without a session", () => {
|
||||
const action = ac.UndesiredEvent({source: "TOP_SITES", event: "MISSING_IMAGE", value: 10});
|
||||
const ping = instance.createUndesiredEvent(action);
|
||||
|
||||
// Is it valid?
|
||||
assertMatchesSchema(ping, UndesiredPing);
|
||||
// Does it have the right value?
|
||||
assert.propertyVal(ping, "value", 10);
|
||||
});
|
||||
it("should create a valid event with a session", () => {
|
||||
const portID = "foo";
|
||||
const data = {source: "TOP_SITES", event: "MISSING_IMAGE", value: 10};
|
||||
const action = ac.SendToMain(ac.UndesiredEvent(data), portID);
|
||||
const session = addSession(portID);
|
||||
const ping = instance.createUndesiredEvent(action);
|
||||
|
||||
// Is it valid?
|
||||
assertMatchesSchema(ping, UndesiredPing);
|
||||
// Does it have the right session_id?
|
||||
assert.propertyVal(ping, "session_id", session.session_id);
|
||||
// Does it have the right value?
|
||||
assert.propertyVal(ping, "value", 10);
|
||||
});
|
||||
});
|
||||
describe("#createPerformanceEvent", () => {
|
||||
it("should create a valid event without a session", () => {
|
||||
const action = ac.PerfEvent({event: "SCREENSHOT_FINISHED", value: 100});
|
||||
const ping = instance.createPerformanceEvent(action);
|
||||
|
||||
// Is it valid?
|
||||
assertMatchesSchema(ping, PerfPing);
|
||||
// Does it have the right value?
|
||||
assert.propertyVal(ping, "value", 100);
|
||||
});
|
||||
it("should create a valid event with a session", () => {
|
||||
const portID = "foo";
|
||||
const data = {event: "PAGE_LOADED", value: 100};
|
||||
const action = ac.SendToMain(ac.PerfEvent(data), portID);
|
||||
const session = addSession(portID);
|
||||
const ping = instance.createPerformanceEvent(action);
|
||||
|
||||
// Is it valid?
|
||||
assertMatchesSchema(ping, PerfPing);
|
||||
// Does it have the right session_id?
|
||||
assert.propertyVal(ping, "session_id", session.session_id);
|
||||
// Does it have the right value?
|
||||
assert.propertyVal(ping, "value", 100);
|
||||
});
|
||||
});
|
||||
describe("#createSessionEndEvent", () => {
|
||||
it("should create a valid event", () => {
|
||||
const ping = instance.createSessionEndEvent({
|
||||
session_id: FAKE_UUID,
|
||||
page: "about:newtab",
|
||||
session_duration: 12345
|
||||
});
|
||||
// Is it valid?
|
||||
assertMatchesSchema(ping, SessionPing);
|
||||
assert.propertyVal(ping, "session_id", FAKE_UUID);
|
||||
assert.propertyVal(ping, "page", "about:newtab");
|
||||
assert.propertyVal(ping, "session_duration", 12345);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("#sendEvent", () => {
|
||||
it("should call telemetrySender", async () => {
|
||||
await instance.init();
|
||||
sandbox.stub(instance.telemetrySender, "sendPing");
|
||||
const event = {};
|
||||
instance.sendEvent(event);
|
||||
assert.calledWith(instance.telemetrySender.sendPing, event);
|
||||
});
|
||||
});
|
||||
describe("#uninit", () => {
|
||||
it("should call .telemetrySender.uninit and remove it", async () => {
|
||||
await instance.init();
|
||||
const stub = sandbox.stub(instance.telemetrySender, "uninit");
|
||||
instance.uninit();
|
||||
assert.calledOnce(stub);
|
||||
assert.isNull(instance.telemetrySender);
|
||||
});
|
||||
});
|
||||
describe("#onAction", () => {
|
||||
it("should call .init() on an INIT action", () => {
|
||||
const stub = sandbox.stub(instance, "init");
|
||||
instance.onAction({type: at.INIT});
|
||||
assert.calledOnce(stub);
|
||||
});
|
||||
it("should call .addSession() on a NEW_TAB_VISIBLE action", () => {
|
||||
const stub = sandbox.stub(instance, "addSession");
|
||||
instance.onAction(ac.SendToMain({type: at.NEW_TAB_VISIBLE}, "port123"));
|
||||
assert.calledWith(stub, "port123");
|
||||
});
|
||||
it("should call .endSession() on a NEW_TAB_UNLOAD action", () => {
|
||||
const stub = sandbox.stub(instance, "endSession");
|
||||
instance.onAction(ac.SendToMain({type: at.NEW_TAB_UNLOAD}, "port123"));
|
||||
assert.calledWith(stub, "port123");
|
||||
});
|
||||
it("should send an event on an TELEMETRY_UNDESIRED_EVENT action", () => {
|
||||
const sendEvent = sandbox.stub(instance, "sendEvent");
|
||||
const eventCreator = sandbox.stub(instance, "createUndesiredEvent");
|
||||
const action = {type: at.TELEMETRY_UNDESIRED_EVENT};
|
||||
instance.onAction(action);
|
||||
assert.calledWith(eventCreator, action);
|
||||
assert.calledWith(sendEvent, eventCreator.returnValue);
|
||||
});
|
||||
it("should send an event on an TELEMETRY_USER_EVENT action", () => {
|
||||
const sendEvent = sandbox.stub(instance, "sendEvent");
|
||||
const eventCreator = sandbox.stub(instance, "createUserEvent");
|
||||
const action = {type: at.TELEMETRY_USER_EVENT};
|
||||
instance.onAction(action);
|
||||
assert.calledWith(eventCreator, action);
|
||||
assert.calledWith(sendEvent, eventCreator.returnValue);
|
||||
});
|
||||
it("should send an event on an TELEMETRY_PERFORMANCE_EVENT action", () => {
|
||||
const sendEvent = sandbox.stub(instance, "sendEvent");
|
||||
const eventCreator = sandbox.stub(instance, "createPerformanceEvent");
|
||||
const action = {type: at.TELEMETRY_PERFORMANCE_EVENT};
|
||||
instance.onAction(action);
|
||||
assert.calledWith(eventCreator, action);
|
||||
assert.calledWith(sendEvent, eventCreator.returnValue);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,7 +2,8 @@
|
|||
// http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
const {GlobalOverrider, FakePrefs} = require("test/unit/utils");
|
||||
const {TelemetrySender} = require("lib/TelemetrySender.jsm");
|
||||
const {TelemetrySender, TelemetrySenderConstants} = require("lib/TelemetrySender.jsm");
|
||||
const {ENDPOINT_PREF, TELEMETRY_PREF, LOGGING_PREF} = TelemetrySenderConstants;
|
||||
|
||||
/**
|
||||
* A reference to the fake preferences object created by the TelemetrySender
|
||||
|
@ -18,27 +19,11 @@ describe("TelemetrySender", () => {
|
|||
let globals;
|
||||
let tSender;
|
||||
let fetchStub;
|
||||
const observerTopics = ["user-action-event", "performance-event",
|
||||
"tab-session-complete", "undesired-event"];
|
||||
const fakeEndpointUrl = "http://127.0.0.1/stuff";
|
||||
const fakePingJSON = JSON.stringify({action: "fake_action", monkey: 1});
|
||||
const fakeFetchHttpErrorResponse = {ok: false, status: 400};
|
||||
const fakeFetchSuccessResponse = {ok: true, status: 200};
|
||||
|
||||
function assertNotificationObserversAdded() {
|
||||
observerTopics.forEach(topic => {
|
||||
assert.calledWithExactly(
|
||||
global.Services.obs.addObserver, tSender, topic, true);
|
||||
});
|
||||
}
|
||||
|
||||
function assertNotificationObserversRemoved() {
|
||||
observerTopics.forEach(topic => {
|
||||
assert.calledWithExactly(
|
||||
global.Services.obs.removeObserver, tSender, topic);
|
||||
});
|
||||
}
|
||||
|
||||
before(() => {
|
||||
globals = new GlobalOverrider();
|
||||
|
||||
|
@ -58,61 +43,52 @@ describe("TelemetrySender", () => {
|
|||
|
||||
after(() => globals.restore());
|
||||
|
||||
it("should construct the Prefs object with the right branch", () => {
|
||||
it("should construct the Prefs object", () => {
|
||||
globals.sandbox.spy(global, "Preferences");
|
||||
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
assert.calledOnce(global.Preferences);
|
||||
assert.calledWith(global.Preferences,
|
||||
sinon.match.has("branch", "browser.newtabpage.activity-stream"));
|
||||
});
|
||||
|
||||
it("should set the enabled prop to false if the pref is false", () => {
|
||||
FakePrefs.prototype.prefs = {telemetry: false};
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;
|
||||
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
assert.isFalse(tSender.enabled);
|
||||
});
|
||||
|
||||
it("should not add notification observers if the enabled pref is false", () => {
|
||||
FakePrefs.prototype.prefs = {telemetry: false};
|
||||
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
assert.notCalled(global.Services.obs.addObserver);
|
||||
});
|
||||
|
||||
it("should set the enabled prop to true if the pref is true", () => {
|
||||
FakePrefs.prototype.prefs = {telemetry: true};
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
|
||||
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
assert.isTrue(tSender.enabled);
|
||||
});
|
||||
|
||||
it("should add all notification observers if the enabled pref is true", () => {
|
||||
FakePrefs.prototype.prefs = {telemetry: true};
|
||||
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
assertNotificationObserversAdded();
|
||||
});
|
||||
|
||||
describe("#_sendPing()", () => {
|
||||
describe("#sendPing()", () => {
|
||||
beforeEach(() => {
|
||||
FakePrefs.prototype.prefs = {
|
||||
"telemetry": true,
|
||||
"telemetry.ping.endpoint": fakeEndpointUrl
|
||||
};
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
|
||||
FakePrefs.prototype.prefs[ENDPOINT_PREF] = fakeEndpointUrl;
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
});
|
||||
|
||||
it("should not send if the TelemetrySender is disabled", async () => {
|
||||
tSender.enabled = false;
|
||||
|
||||
await tSender.sendPing(fakePingJSON);
|
||||
|
||||
assert.notCalled(fetchStub);
|
||||
});
|
||||
|
||||
it("should POST given ping data to telemetry.ping.endpoint pref w/fetch",
|
||||
async () => {
|
||||
fetchStub.resolves(fakeFetchSuccessResponse);
|
||||
await tSender._sendPing(fakePingJSON);
|
||||
await tSender.sendPing(fakePingJSON);
|
||||
|
||||
assert.calledOnce(fetchStub);
|
||||
assert.calledWithExactly(fetchStub, fakeEndpointUrl,
|
||||
|
@ -122,7 +98,7 @@ describe("TelemetrySender", () => {
|
|||
it("should log HTTP failures using Cu.reportError", async () => {
|
||||
fetchStub.resolves(fakeFetchHttpErrorResponse);
|
||||
|
||||
await tSender._sendPing(fakePingJSON);
|
||||
await tSender.sendPing(fakePingJSON);
|
||||
|
||||
assert.called(Components.utils.reportError);
|
||||
});
|
||||
|
@ -130,79 +106,42 @@ describe("TelemetrySender", () => {
|
|||
it("should log an error using Cu.reportError if fetch rejects", async () => {
|
||||
fetchStub.rejects("Oh noes!");
|
||||
|
||||
await tSender._sendPing(fakePingJSON);
|
||||
await tSender.sendPing(fakePingJSON);
|
||||
|
||||
assert.called(Components.utils.reportError);
|
||||
});
|
||||
|
||||
it("should log if logging is on && if action is not activity_stream_performance", async () => {
|
||||
FakePrefs.prototype.prefs = {
|
||||
"telemetry": true,
|
||||
"performance.log": true
|
||||
};
|
||||
globals.sandbox.stub(console, "log");
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
|
||||
FakePrefs.prototype.prefs[LOGGING_PREF] = true;
|
||||
fetchStub.resolves(fakeFetchSuccessResponse);
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
await tSender._sendPing(fakePingJSON);
|
||||
await tSender.sendPing(fakePingJSON);
|
||||
|
||||
assert.called(console.log); // eslint-disable-line no-console
|
||||
});
|
||||
});
|
||||
|
||||
describe("#observe()", () => {
|
||||
before(() => {
|
||||
globals.sandbox.stub(TelemetrySender.prototype, "_sendPing");
|
||||
});
|
||||
|
||||
observerTopics.forEach(topic => {
|
||||
it(`should call this._sendPing with data for ${topic}`, () => {
|
||||
const fakeSubject = "fakeSubject";
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
tSender.observe(fakeSubject, topic, fakePingJSON);
|
||||
|
||||
assert.calledOnce(TelemetrySender.prototype._sendPing);
|
||||
assert.calledWithExactly(TelemetrySender.prototype._sendPing,
|
||||
fakePingJSON);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call this._sendPing for 'nonexistent-topic'", () => {
|
||||
const fakeSubject = "fakeSubject";
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
tSender.observe(fakeSubject, "nonexistent-topic", fakePingJSON);
|
||||
|
||||
assert.notCalled(TelemetrySender.prototype._sendPing);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#uninit()", () => {
|
||||
it("should remove the telemetry pref listener", () => {
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
assert.property(fakePrefs.observers, "telemetry");
|
||||
assert.property(fakePrefs.observers, TELEMETRY_PREF);
|
||||
|
||||
tSender.uninit();
|
||||
|
||||
assert.notProperty(fakePrefs.observers, "telemetry");
|
||||
assert.notProperty(fakePrefs.observers, TELEMETRY_PREF);
|
||||
});
|
||||
|
||||
it("should remove all notification observers if telemetry pref is true", () => {
|
||||
FakePrefs.prototype.prefs = {telemetry: true};
|
||||
it("should remove the telemetry log listener", () => {
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
assert.property(fakePrefs.observers, LOGGING_PREF);
|
||||
|
||||
tSender.uninit();
|
||||
|
||||
assertNotificationObserversRemoved();
|
||||
});
|
||||
|
||||
it("should not remove notification observers if telemetry pref is false", () => {
|
||||
FakePrefs.prototype.prefs = {telemetry: false};
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
|
||||
tSender.uninit();
|
||||
|
||||
assert.notCalled(global.Services.obs.removeObserver);
|
||||
assert.notProperty(fakePrefs.observers, TELEMETRY_PREF);
|
||||
});
|
||||
|
||||
it("should call Cu.reportError if this._prefs.ignore throws", () => {
|
||||
|
@ -218,51 +157,42 @@ describe("TelemetrySender", () => {
|
|||
describe("Misc pref changes", () => {
|
||||
describe("telemetry changes from true to false", () => {
|
||||
beforeEach(() => {
|
||||
FakePrefs.prototype.prefs = {"telemetry": true};
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
assert.propertyVal(tSender, "enabled", true);
|
||||
});
|
||||
|
||||
it("should set the enabled property to false", () => {
|
||||
fakePrefs.set("telemetry", false);
|
||||
fakePrefs.set(TELEMETRY_PREF, false);
|
||||
|
||||
assert.propertyVal(tSender, "enabled", false);
|
||||
});
|
||||
|
||||
it("should remove all notification observers", () => {
|
||||
fakePrefs.set("telemetry", false);
|
||||
|
||||
assertNotificationObserversRemoved();
|
||||
});
|
||||
});
|
||||
|
||||
describe("telemetry changes from false to true", () => {
|
||||
beforeEach(() => {
|
||||
FakePrefs.prototype.prefs = {"telemetry": false};
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
assert.propertyVal(tSender, "enabled", false);
|
||||
});
|
||||
|
||||
it("should set the enabled property to true", () => {
|
||||
fakePrefs.set("telemetry", true);
|
||||
fakePrefs.set(TELEMETRY_PREF, true);
|
||||
|
||||
assert.propertyVal(tSender, "enabled", true);
|
||||
});
|
||||
|
||||
it("should add all topic observers", () => {
|
||||
fakePrefs.set("telemetry", true);
|
||||
|
||||
assertNotificationObserversAdded();
|
||||
});
|
||||
});
|
||||
|
||||
describe("performance.log changes from false to true", () => {
|
||||
it("should change this.logging from false to true", () => {
|
||||
FakePrefs.prototype.prefs = {"performance.log": false};
|
||||
FakePrefs.prototype.prefs = {};
|
||||
FakePrefs.prototype.prefs[LOGGING_PREF] = false;
|
||||
tSender = new TelemetrySender(tsArgs);
|
||||
assert.propertyVal(tSender, "logging", false);
|
||||
|
||||
fakePrefs.set("performance.log", true);
|
||||
fakePrefs.set(LOGGING_PREF, true);
|
||||
|
||||
assert.propertyVal(tSender, "logging", true);
|
||||
});
|
||||
|
|
|
@ -112,5 +112,27 @@ describe("Top Sites Feed", () => {
|
|||
feed.onAction({type: at.NEW_TAB_LOAD});
|
||||
assert.notCalled(feed.refresh);
|
||||
});
|
||||
it("should call openNewWindow with the correct url on OPEN_NEW_WINDOW", () => {
|
||||
sinon.stub(feed, "openNewWindow");
|
||||
const openWindowAction = {type: at.OPEN_NEW_WINDOW, data: {url: "foo.com"}};
|
||||
feed.onAction(openWindowAction);
|
||||
assert.calledWith(feed.openNewWindow, openWindowAction);
|
||||
});
|
||||
it("should call openNewWindow with the correct url and privacy args on OPEN_PRIVATE_WINDOW", () => {
|
||||
sinon.stub(feed, "openNewWindow");
|
||||
const openWindowAction = {type: at.OPEN_PRIVATE_WINDOW, data: {url: "foo.com"}};
|
||||
feed.onAction(openWindowAction);
|
||||
assert.calledWith(feed.openNewWindow, openWindowAction, true);
|
||||
});
|
||||
it("should call openNewWindow with the correct url on OPEN_NEW_WINDOW", () => {
|
||||
const openWindowAction = {
|
||||
type: at.OPEN_NEW_WINDOW,
|
||||
data: {url: "foo.com"},
|
||||
_target: {browser: {ownerGlobal: {openLinkIn: () => {}}}}
|
||||
};
|
||||
sinon.stub(openWindowAction._target.browser.ownerGlobal, "openLinkIn");
|
||||
feed.onAction(openWindowAction);
|
||||
assert.calledOnce(openWindowAction._target.browser.ownerGlobal.openLinkIn);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,8 @@ overrider.set({
|
|||
utils: {
|
||||
import: overrider.sandbox.spy(),
|
||||
importGlobalProperties: overrider.sandbox.spy(),
|
||||
reportError: overrider.sandbox.spy()
|
||||
reportError: overrider.sandbox.spy(),
|
||||
now: () => window.performance.now()
|
||||
}
|
||||
},
|
||||
XPCOMUtils: {
|
||||
|
@ -21,9 +22,14 @@ overrider.set({
|
|||
defineLazyServiceGetter: overrider.sandbox.spy(),
|
||||
generateQI: overrider.sandbox.stub().returns(() => {})
|
||||
},
|
||||
console: {log: overrider.sandbox.spy()},
|
||||
dump: overrider.sandbox.spy(),
|
||||
fetch: overrider.sandbox.stub(),
|
||||
Services: {
|
||||
locale: {getRequestedLocale: overrider.sandbox.stub()},
|
||||
mm: {
|
||||
addMessageListener: overrider.sandbox.spy((msg, cb) => cb()),
|
||||
removeMessageListener: overrider.sandbox.spy()
|
||||
},
|
||||
obs: {
|
||||
addObserver: overrider.sandbox.spy(),
|
||||
removeObserver: overrider.sandbox.spy()
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
const React = require("react");
|
||||
const {mount, shallow} = require("enzyme");
|
||||
const {IntlProvider, intlShape} = require("react-intl");
|
||||
const messages = require("data/locales.json")["en-US"];
|
||||
const intlProvider = new IntlProvider({locale: "en", messages});
|
||||
const {intl} = intlProvider.getChildContext();
|
||||
|
||||
/**
|
||||
* GlobalOverrider - Utility that allows you to override properties on the global object.
|
||||
* See unit-entry.js for example usage.
|
||||
|
@ -115,8 +122,28 @@ function addNumberReducer(prevState = 0, action) {
|
|||
return action.type === "ADD" ? prevState + action.data : prevState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functions to test components that need IntlProvider as an ancestor
|
||||
*/
|
||||
function nodeWithIntlProp(node) {
|
||||
return React.cloneElement(node, {intl});
|
||||
}
|
||||
|
||||
function shallowWithIntl(node) {
|
||||
return shallow(nodeWithIntlProp(node), {context: {intl}});
|
||||
}
|
||||
|
||||
function mountWithIntl(node) {
|
||||
return mount(nodeWithIntlProp(node), {
|
||||
context: {intl},
|
||||
childContextTypes: {intl: intlShape}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FakePrefs,
|
||||
GlobalOverrider,
|
||||
addNumberReducer
|
||||
addNumberReducer,
|
||||
mountWithIntl,
|
||||
shallowWithIntl
|
||||
};
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
Copyright 2014 Yahoo Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the Yahoo Inc. nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|