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 : afc2d1f4a50c3b1253d8f19db9550edb88043532
This commit is contained in:
Ed Lee 2017-05-09 16:09:43 -07:00
Родитель e4d334a9c1
Коммит 6f67b5e149
37 изменённых файлов: 5696 добавлений и 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", () => {

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

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

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