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
This commit is contained in:
Ed Lee 2017-05-09 16:09:43 -07:00
Родитель 0877049e58
Коммит 0d0539754c
43 изменённых файлов: 6028 добавлений и 374 удалений

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

@ -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
};

27
browser/extensions/activity-stream/vendor/REACT_INTL_LICENSE поставляемый Normal file
Просмотреть файл

@ -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.

9
browser/extensions/activity-stream/vendor/react-intl.js поставляемый Normal file

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