Fix Bug 1402654 - Allow custom image / thumbnail for an edited top site.

This commit is contained in:
Andrei Oprea 2018-03-23 12:30:32 +01:00 коммит произвёл GitHub
Родитель 9632262414
Коммит b69291f6f3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
16 изменённых файлов: 760 добавлений и 89 удалений

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

@ -179,6 +179,7 @@ and losing focus. | :one:
| `topsites_data_late_by_ms` | [Optional] Time in ms it took for TopSites to become initialized | :one:
| `topstories.domain.affinity.calculation.ms` | [Optional] Time in ms it took for domain affinities to be calculated | :one:
| `topsites_first_painted_ts` | [Optional][Service Counter][Server Alert for too many omissions] Timestamp of when the Top Sites element finished painting (possibly with only placeholder screenshots) | :one:
| `custom_screenshot` | [Optional] Number of topsites that display a custom screenshot. | :one:
| `screenshot_with_icon` | [Optional] Number of topsites that display a screenshot and a favicon. | :one:
| `screenshot` | [Optional] Number of topsites that display only a screenshot. | :one:
| `tippytop` | [Optional] Number of topsites that display a tippytop icon. | :one:

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

@ -284,6 +284,15 @@ A user event ping includes some basic metadata (tab id, addon version, etc.) as
}
```
#### Requesting a custom screenshot preview
```js
{
"event": "PREVIEW_REQUEST",
"source": "TOP_SITES"
}
```
## Session end pings
When a session ends, the browser will send a `"activity_stream_session"` ping to our metrics servers. This ping contains the length of the session, a unique reason for why the session ended, and some additional metadata.
@ -349,9 +358,10 @@ perf: {
// and is therefore just showing placeholder screenshots.
"topsites_first_painted_ts": 5,
// The 5 different types of TopSites icons and how many of each kind did the
// The 6 different types of TopSites icons and how many of each kind did the
// user see.
"topsites_icon_stats": {
"custom_screenshot": 0,
"screenshot_with_icon": 2,
"screenshot": 1,
"tippytop": 2,

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

@ -58,6 +58,9 @@ for (const type of [
"PLACES_SAVED_TO_POCKET",
"PREFS_INITIAL_VALUES",
"PREF_CHANGED",
"PREVIEW_REQUEST",
"PREVIEW_REQUEST_CANCEL",
"PREVIEW_RESPONSE",
"RICH_ICON_MISSING",
"SAVE_SESSION_PERF_DATA",
"SAVE_TO_POCKET",

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

@ -92,9 +92,46 @@ function TopSites(prevState = INITIAL_STATE.TopSites, action) {
case at.TOP_SITES_PREFS_UPDATED:
return Object.assign({}, prevState, {pref: action.data.pref});
case at.TOP_SITES_EDIT:
return Object.assign({}, prevState, {editForm: {index: action.data.index}});
return Object.assign({}, prevState, {
editForm: {
index: action.data.index,
previewResponse: null
}
});
case at.TOP_SITES_CANCEL_EDIT:
return Object.assign({}, prevState, {editForm: null});
case at.PREVIEW_RESPONSE:
if (!prevState.editForm || action.data.url !== prevState.editForm.previewUrl) {
return prevState;
}
return Object.assign({}, prevState, {
editForm: {
index: prevState.editForm.index,
previewResponse: action.data.preview,
previewUrl: action.data.url
}
});
case at.PREVIEW_REQUEST:
if (!prevState.editForm) {
return prevState;
}
return Object.assign({}, prevState, {
editForm: {
index: prevState.editForm.index,
previewResponse: null,
previewUrl: action.data.url
}
});
case at.PREVIEW_REQUEST_CANCEL:
if (!prevState.editForm) {
return prevState;
}
return Object.assign({}, prevState, {
editForm: {
index: prevState.editForm.index,
previewResponse: null
}
});
case at.SCREENSHOT_UPDATED:
newRows = prevState.rows.map(row => {
if (row && row.url === action.data.url) {

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

@ -58,7 +58,7 @@ export class TopSiteLink extends React.PureComponent {
}
render() {
const {children, className, isDraggable, link, onClick, title} = this.props;
const {children, className, defaultStyle, isDraggable, link, onClick, title} = this.props;
const topSiteOuterClassName = `top-site-outer${className ? ` ${className}` : ""}${link.isDragged ? " dragged" : ""}`;
const {tippyTopIcon, faviconSize} = link;
const [letterFallback] = title;
@ -67,7 +67,16 @@ export class TopSiteLink extends React.PureComponent {
let showSmallFavicon = false;
let smallFaviconStyle;
let smallFaviconFallback;
if (tippyTopIcon || faviconSize >= MIN_RICH_FAVICON_SIZE) {
if (defaultStyle) { // force no styles (letter fallback) even if the link has imagery
smallFaviconFallback = false;
} else if (link.customScreenshotURL) {
// assume high quality custom screenshot and use rich icon styles and class names
imageClassName = "top-site-icon rich-icon";
imageStyle = {
backgroundColor: link.backgroundColor,
backgroundImage: `url(${link.screenshot})`
};
} else if (tippyTopIcon || faviconSize >= MIN_RICH_FAVICON_SIZE) {
// styles and class names for top sites with rich icons
imageClassName = "top-site-icon rich-icon";
imageStyle = {
@ -288,7 +297,15 @@ export class _TopSiteList extends React.PureComponent {
this.dropped = true;
this.props.dispatch(ac.AlsoToMain({
type: at.TOP_SITES_INSERT,
data: {site: {url: this.state.draggedSite.url, label: this.state.draggedTitle}, index, draggedFromIndex: this.state.draggedIndex}
data: {
site: {
url: this.state.draggedSite.url,
label: this.state.draggedTitle,
customScreenshotURL: this.state.draggedSite.customScreenshotURL
},
index,
draggedFromIndex: this.state.draggedIndex
}
}));
this.userEvent("DROP", index);
}

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

@ -12,13 +12,20 @@ export class TopSiteForm extends React.PureComponent {
this.state = {
label: site ? (site.label || site.hostname) : "",
url: site ? site.url : "",
validationError: false
validationError: false,
customScreenshotUrl: site ? site.customScreenshotURL : "",
showCustomScreenshotForm: site ? site.customScreenshotURL : false
};
this.onClearScreenshotInput = this.onClearScreenshotInput.bind(this);
this.onLabelChange = this.onLabelChange.bind(this);
this.onUrlChange = this.onUrlChange.bind(this);
this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
this.onClearUrlClick = this.onClearUrlClick.bind(this);
this.onDoneButtonClick = this.onDoneButtonClick.bind(this);
this.onCustomScreenshotUrlChange = this.onCustomScreenshotUrlChange.bind(this);
this.onPreviewButtonClick = this.onPreviewButtonClick.bind(this);
this.onEnableScreenshotUrlForm = this.onEnableScreenshotUrlForm.bind(this);
this.validateUrl = this.validateUrl.bind(this);
}
onLabelChange(event) {
@ -39,6 +46,26 @@ export class TopSiteForm extends React.PureComponent {
});
}
onEnableScreenshotUrlForm() {
this.setState({showCustomScreenshotForm: true});
}
_updateCustomScreenshotInput(customScreenshotUrl) {
this.setState({
customScreenshotUrl,
validationError: false
});
this.props.dispatch({type: at.PREVIEW_REQUEST_CANCEL});
}
onCustomScreenshotUrlChange(event) {
this._updateCustomScreenshotInput(event.target.value);
}
onClearScreenshotInput() {
this._updateCustomScreenshotInput("");
}
onCancelButtonClick(ev) {
ev.preventDefault();
this.props.onClose();
@ -54,6 +81,12 @@ export class TopSiteForm extends React.PureComponent {
site.label = this.state.label;
}
if (this.state.customScreenshotUrl) {
site.customScreenshotURL = this.cleanUrl(this.state.customScreenshotUrl);
} else if (this.props.site && this.props.site.customScreenshotURL) {
// Used to flag that previously cached screenshot should be removed
site.customScreenshotURL = null;
}
this.props.dispatch(ac.AlsoToMain({
type: at.TOP_SITES_PIN,
data: {site, index}
@ -68,6 +101,20 @@ export class TopSiteForm extends React.PureComponent {
}
}
onPreviewButtonClick(event) {
event.preventDefault();
if (this.validateForm()) {
this.props.dispatch(ac.AlsoToMain({
type: at.PREVIEW_REQUEST,
data: {url: this.cleanUrl(this.state.customScreenshotUrl)}
}));
this.props.dispatch(ac.UserEvent({
source: TOP_SITES_SOURCE,
event: "PREVIEW_REQUEST"
}));
}
}
cleanUrl(url) {
// If we are missing a protocol, prepend http://
if (!url.startsWith("http:") && !url.startsWith("https:")) {
@ -84,16 +131,66 @@ export class TopSiteForm extends React.PureComponent {
}
}
validateCustomScreenshotUrl() {
const {customScreenshotUrl} = this.state;
return !customScreenshotUrl || this.validateUrl(customScreenshotUrl);
}
validateForm() {
const validate = this.validateUrl(this.state.url);
this.setState({validationError: !validate});
const validate = this.validateUrl(this.state.url) && this.validateCustomScreenshotUrl();
if (!validate) {
this.setState({validationError: true});
}
return validate;
}
_renderCustomScreenshotInput() {
const {customScreenshotUrl} = this.state;
const requestFailed = this.props.previewResponse === "";
const validationError = (this.state.validationError && !this.validateCustomScreenshotUrl()) || requestFailed;
// Set focus on error if the url field is valid or when the input is first rendered and is empty
const shouldFocus = (validationError && this.validateUrl(this.state.url)) || !customScreenshotUrl;
const isLoading = this.props.previewResponse === null &&
customScreenshotUrl && this.props.previewUrl === this.cleanUrl(customScreenshotUrl);
if (!this.state.showCustomScreenshotForm) {
return (<a className="enable-custom-image-input" onClick={this.onEnableScreenshotUrlForm}>
<FormattedMessage id="topsites_form_use_image_link" />
</a>);
}
return (<div className="custom-image-input-container">
<TopSiteFormInput
errorMessageId={requestFailed ? "topsites_form_image_validation" : "topsites_form_url_validation"}
loading={isLoading}
onChange={this.onCustomScreenshotUrlChange}
onClear={this.onClearScreenshotInput}
shouldFocus={shouldFocus}
typeUrl={true}
value={customScreenshotUrl}
validationError={validationError}
titleId="topsites_form_image_url_label"
placeholderId="topsites_form_url_placeholder"
intl={this.props.intl} />
</div>);
}
render() {
const {customScreenshotUrl} = this.state;
const requestFailed = this.props.previewResponse === "";
// For UI purposes, editing without an existing link is "add"
const showAsAdd = !this.props.site;
const previous = (this.props.site && this.props.site.customScreenshotURL) || "";
const changed = customScreenshotUrl && this.cleanUrl(customScreenshotUrl) !== previous;
// Preview mode if changes were made to the custom screenshot URL and no preview was received yet
// or the request failed
const previewMode = changed && !this.props.previewResponse;
const previewLink = Object.assign({}, this.props.site);
if (this.props.previewResponse) {
previewLink.screenshot = this.props.previewResponse;
previewLink.customScreenshotURL = this.props.previewUrl;
}
return (
<form className="topsite-form">
<div className="form-input-container">
@ -108,25 +205,33 @@ export class TopSiteForm extends React.PureComponent {
placeholderId="topsites_form_title_placeholder"
intl={this.props.intl} />
<TopSiteFormInput onChange={this.onUrlChange}
shouldFocus={this.state.validationError && !this.validateUrl(this.state.url)}
value={this.state.url}
onClear={this.onClearUrlClick}
validationError={this.state.validationError}
validationError={this.state.validationError && !this.validateUrl(this.state.url)}
titleId="topsites_form_url_label"
typeUrl={true}
placeholderId="topsites_form_url_placeholder"
errorMessageId="topsites_form_url_validation"
intl={this.props.intl} />
{this._renderCustomScreenshotInput()}
</div>
<TopSiteLink link={this.props.site || {}} title={this.state.label} />
<TopSiteLink link={previewLink}
defaultStyle={requestFailed}
title={this.state.label} />
</div>
</div>
<section className="actions">
<button className="cancel" type="button" onClick={this.onCancelButtonClick}>
<FormattedMessage id="topsites_form_cancel_button" />
</button>
<button className="done" type="submit" onClick={this.onDoneButtonClick}>
<FormattedMessage id={showAsAdd ? "topsites_form_add_button" : "topsites_form_save_button"} />
</button>
{previewMode ?
<button className="done preview" type="submit" onClick={this.onPreviewButtonClick}>
<FormattedMessage id="topsites_form_preview_button" />
</button> :
<button className="done" type="submit" onClick={this.onDoneButtonClick}>
<FormattedMessage id={showAsAdd ? "topsites_form_add_button" : "topsites_form_save_button"} />
</button>}
</section>
</form>
);
@ -134,6 +239,6 @@ export class TopSiteForm extends React.PureComponent {
}
TopSiteForm.defaultProps = {
TopSite: null,
site: null,
index: -1
};

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

@ -4,13 +4,29 @@ import React from "react";
export class TopSiteFormInput extends React.PureComponent {
constructor(props) {
super(props);
this.state = {validationError: this.props.validationError};
this.onChange = this.onChange.bind(this);
this.onMount = this.onMount.bind(this);
}
componentWillReceiveProps(nextProps) {
if (nextProps.validationError && !this.props.validationError) {
if (nextProps.shouldFocus && !this.props.shouldFocus) {
this.input.focus();
}
if (nextProps.validationError && !this.props.validationError) {
this.setState({validationError: true});
}
// If the component is in an error state but the value was cleared by the parent
if (this.state.validationError && !nextProps.value) {
this.setState({validationError: false});
}
}
onChange(ev) {
if (this.state.validationError) {
this.setState({validationError: false});
}
this.props.onChange(ev);
}
onMount(input) {
@ -19,17 +35,21 @@ export class TopSiteFormInput extends React.PureComponent {
render() {
const showClearButton = this.props.value && this.props.onClear;
const {validationError, typeUrl} = this.props;
const {typeUrl} = this.props;
const {validationError} = this.state;
return (<label><FormattedMessage id={this.props.titleId} />
<div className={`field ${typeUrl ? "url" : ""}${validationError ? " invalid" : ""}`}>
{showClearButton &&
<div className="icon icon-clear-input" onClick={this.props.onClear} />}
{this.props.loading ?
<div className="loading-container"><div className="loading-animation" /></div> :
showClearButton && <div className="icon icon-clear-input" onClick={this.props.onClear} />}
<input type="text"
value={this.props.value}
ref={this.onMount}
onChange={this.props.onChange}
placeholder={this.props.intl.formatMessage({id: this.props.placeholderId})} />
onChange={this.onChange}
placeholder={this.props.intl.formatMessage({id: this.props.placeholderId})}
autoFocus={this.props.shouldFocus}
disabled={this.props.loading} />
{validationError &&
<aside className="error-tooltip">
<FormattedMessage id={this.props.errorMessageId} />

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

@ -10,6 +10,9 @@ import {TopSiteForm} from "./TopSiteForm";
import {TopSiteList} from "./TopSite";
function topSiteIconType(link) {
if (link.customScreenshotURL) {
return "custom_screenshot";
}
if (link.tippyTopIcon || link.faviconRef === "tippytop") {
return "tippytop";
}
@ -37,6 +40,7 @@ function countTopSitesIconsTypes(topSites) {
};
return topSites.reduce(countTopSitesTypes, {
"custom_screenshot": 0,
"screenshot_with_icon": 0,
"screenshot": 0,
"tippytop": 0,
@ -119,10 +123,10 @@ export class _TopSites extends React.PureComponent {
<div className="modal">
<TopSiteForm
site={props.TopSites.rows[editForm.index]}
index={editForm.index}
onClose={this.onFormClose}
dispatch={this.props.dispatch}
intl={this.props.intl} />
intl={this.props.intl}
{...editForm} />
</div>
</div>
}

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

@ -153,7 +153,7 @@ $half-base-gutter: $base-gutter / 2;
}
.rich-icon {
background-size: $rich-icon-size;
background-size: cover;
height: 100%;
offset-inline-start: 0;
top: 0;
@ -302,7 +302,7 @@ $half-base-gutter: $base-gutter / 2;
.form-input-container {
max-width: $form-width + 3 * $form-spacing + $rich-icon-size;
margin: 0 auto;
padding: $form-spacing $form-spacing 40px;
padding: $form-spacing;
.top-site-outer {
padding: 0;
@ -337,20 +337,71 @@ $half-base-gutter: $base-gutter / 2;
transform: translateY(-50%);
top: 50%;
offset-inline-end: 8px;
}
}
& + input:dir(ltr) {
padding-right: 32px;
}
.url {
input:dir(ltr) {
padding-right: 32px;
}
& + input:dir(rtl) {
padding-left: 32px;
input:dir(rtl) {
padding-left: 32px;
&:not(:placeholder-shown) {
direction: ltr;
text-align: right;
}
}
}
.url input:not(:placeholder-shown):dir(rtl) {
direction: ltr;
text-align: right;
.enable-custom-image-input {
display: inline-block;
font-size: 13px;
margin-top: 4px;
cursor: pointer;
&:hover {
text-decoration: underline;
color: $blue-60;
}
}
.custom-image-input-container {
margin-top: 4px;
.loading-container {
width: 16px;
height: 16px;
overflow: hidden;
position: absolute;
transform: translateY(-50%);
top: 50%;
offset-inline-end: 8px;
}
// This animation is derived from Firefox's tab loading animation
// See https://searchfox.org/mozilla-central/rev/b29daa46443b30612415c35be0a3c9c13b9dc5f6/browser/themes/shared/tabs.inc.css#208-216
.loading-animation {
@keyframes tab-throbber-animation {
100% { transform: translateX(-960px); }
}
@keyframes tab-throbber-animation-rtl {
100% { transform: translateX(960px); }
}
width: 960px;
height: 16px;
-moz-context-properties: fill;
fill: $blue-50;
background-image: url('chrome://browser/skin/tabbrowser/loading.svg');
animation: tab-throbber-animation 1.05s steps(60) infinite;
&:dir(rtl) {
animation-name: tab-throbber-animation-rtl;
}
}
}
input {
@ -361,11 +412,18 @@ $half-base-gutter: $base-gutter / 2;
padding: 0 8px;
height: 32px;
width: 100%;
font-size: 15px;
&:focus {
border: $input-border-active;
box-shadow: $input-focus-boxshadow;
}
&[disabled] {
border: $input-border;
box-shadow: none;
opacity: 0.4;
}
}
}

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

@ -55,8 +55,8 @@ $text-secondary: $grey-50;
$input-border: solid 1px $grey-90-20;
$input-border-active: solid 1px $blue-50;
$input-error-border: solid 1px $red-60;
$input-error-boxshadow: 0 0 0 2px rgba($red-60, 0.35);
$input-focus-boxshadow: 0 0 0 2px rgba($blue-50, 0.35);
$input-error-boxshadow: 0 0 0 1px $red-60, 0 0 0 4px rgba($red-60, 0.3);
$input-focus-boxshadow: 0 0 0 1px $blue-50, 0 0 0 4px rgba($blue-50, 0.3);
$white: #FFF;
$border-radius: 3px;

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

@ -27,7 +27,7 @@ const DEFAULT_SITES_PREF = "default.sites";
const DEFAULT_TOP_SITES = [];
const FRECENCY_THRESHOLD = 100 + 1; // 1 visit (skip first-run/one-time pages)
const MIN_FAVICON_SIZE = 96;
const CACHED_LINK_PROPS_TO_MIGRATE = ["screenshot"];
const CACHED_LINK_PROPS_TO_MIGRATE = ["screenshot", "customScreenshot"];
const PINNED_FAVICON_PROPS_TO_MIGRATE = ["favicon", "faviconRef", "faviconSize"];
const SECTION_ID = "topsites";
@ -72,7 +72,13 @@ this.TopSitesFeed = class TopSitesFeed {
filterForThumbnailExpiration(callback) {
const {rows} = this.store.getState().TopSites;
callback(rows.map(site => site.url));
callback(rows.reduce((acc, site) => {
acc.push(site.url);
if (site.customScreenshotURL) {
acc.push(site.customScreenshotURL);
}
return acc;
}, []));
}
async getLinksWithDefaults(action) {
@ -134,7 +140,12 @@ this.TopSitesFeed = class TopSitesFeed {
// Now, get a tippy top icon, a rich icon, or screenshot for every item
for (const link of withPinned) {
if (link) {
this._fetchIcon(link);
// If there is a custom screenshot this is the only image we display
if (link.customScreenshotURL) {
this._fetchScreenshot(link, link.customScreenshotURL);
} else {
this._fetchIcon(link);
}
// Remove internal properties that might be updated after dispatch
delete link.__sharedCache;
@ -190,14 +201,36 @@ this.TopSitesFeed = class TopSitesFeed {
this._requestRichIcon(link.url);
// Also request a screenshot if we don't have one yet
if (!link.screenshot) {
const {url} = link;
await Screenshots.maybeCacheScreenshot(link, url, "screenshot",
screenshot => this.store.dispatch(ac.BroadcastToContent({
data: {screenshot, url},
type: at.SCREENSHOT_UPDATED
})));
await this._fetchScreenshot(link, link.url);
}
/**
* Fetch, cache and broadcast a screenshot for a specific topsite.
* @param link cached topsite object
* @param url where to fetch the image from
*/
async _fetchScreenshot(link, url) {
if (link.screenshot) {
return;
}
await Screenshots.maybeCacheScreenshot(link, url, "screenshot",
screenshot => this.store.dispatch(ac.BroadcastToContent({
data: {screenshot, url: link.url},
type: at.SCREENSHOT_UPDATED
})));
}
/**
* Dispatch screenshot preview to target or notify if request failed.
* @param customScreenshotURL {string} The URL used to capture the screenshot
* @param target {string} Id of content process where to dispatch the result
*/
async getScreenshotPreview(url, target) {
const preview = await Screenshots.getScreenshotForURL(url) || "";
this.store.dispatch(ac.OnlyToOneContent({
data: {url, preview},
type: at.PREVIEW_RESPONSE
}, target));
}
_requestRichIcon(url) {
@ -224,23 +257,41 @@ this.TopSitesFeed = class TopSitesFeed {
/**
* Pin a site at a specific position saving only the desired keys.
* @param customScreenshotURL {string} User set URL of preview image for site
* @param label {string} User set string of custom site name
*/
_pinSiteAt({label, url}, index) {
async _pinSiteAt({customScreenshotURL, label, url}, index) {
const toPin = {url};
if (label) {
toPin.label = label;
}
if (customScreenshotURL) {
toPin.customScreenshotURL = customScreenshotURL;
}
NewTabUtils.pinnedLinks.pin(toPin, index);
await this._clearLinkCustomScreenshot({customScreenshotURL, url});
}
async _clearLinkCustomScreenshot(site) {
// If screenshot url changed or was removed we need to update the cached link obj
if (site.customScreenshotURL !== undefined) {
const pinned = await this.pinnedCache.request();
const link = pinned.find(pin => pin && pin.url === site.url);
if (link && link.customScreenshotURL !== site.customScreenshotURL) {
link.__sharedCache.updateLink("screenshot", undefined);
}
}
}
/**
* Handle a pin action of a site to a position.
*/
pin(action) {
async pin(action) {
const {site, index} = action.data;
// If valid index provided, pin at that position
if (index >= 0) {
this._pinSiteAt(site, index);
await this._pinSiteAt(site, index);
this._broadcastPinnedSitesUpdated();
} else {
this.insert(action);
@ -301,7 +352,7 @@ this.TopSitesFeed = class TopSitesFeed {
/**
* Handle an insert (drop/add) action of a site.
*/
insert(action) {
async insert(action) {
let {index} = action.data;
// Treat invalid pin index values (e.g., -1, undefined) as insert in the first position
if (!(index > 0)) {
@ -313,6 +364,8 @@ this.TopSitesFeed = class TopSitesFeed {
this._insertPin(
action.data.site, index,
action.data.draggedFromIndex !== undefined ? action.data.draggedFromIndex : this.store.getState().Prefs.values.topSitesRows * TOP_SITES_MAX_SITES_PER_ROW);
await this._clearLinkCustomScreenshot(action.data.site);
this._broadcastPinnedSitesUpdated();
}
@ -358,6 +411,9 @@ this.TopSitesFeed = class TopSitesFeed {
case at.TOP_SITES_INSERT:
this.insert(action);
break;
case at.PREVIEW_REQUEST:
this.getScreenshotPreview(action.data.url, action.meta.fromTarget);
break;
case at.UNINIT:
this.uninit();
break;

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

@ -76,10 +76,12 @@ export const UserEventAction = Joi.object().keys({
"BOOKMARK_DELETE",
"BOOKMARK_ADD",
"PIN",
"PREVIEW_REQUEST",
"UNPIN",
"SAVE_TO_POCKET",
"SECTION_MENU_MOVE_UP",
"SECTION_MENU_MOVE_DOWN",
"SCREENSHOT_REQUEST",
"SECTION_MENU_REMOVE",
"SECTION_MENU_COLLAPSE",
"SECTION_MENU_EXPAND",
@ -170,6 +172,7 @@ export const SessionPing = Joi.object().keys(Object.assign({}, baseKeys, {
// Information about the quality of TopSites images and icons.
topsites_icon_stats: Joi.object().keys({
custom_screenshot: Joi.number(),
rich_icon: Joi.number(),
screenshot: Joi.number(),
screenshot_with_icon: Joi.number(),

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

@ -54,6 +54,48 @@ describe("Reducers", () => {
const nextState = TopSites(undefined, {type: at.TOP_SITES_CANCEL_EDIT});
assert.isNull(nextState.editForm);
});
it("should preserve the editForm.index", () => {
const actionTypes = [at.PREVIEW_RESPONSE, at.PREVIEW_REQUEST, at.PREVIEW_REQUEST_CANCEL];
actionTypes.forEach(type => {
const oldState = {editForm: {index: 0, previewUrl: "foo"}};
const action = {type, data: {url: "foo"}};
const nextState = TopSites(oldState, action);
assert.equal(nextState.editForm.index, 0);
});
});
it("should set previewResponse on PREVIEW_RESPONSE", () => {
const oldState = {editForm: {previewUrl: "url"}};
const action = {type: at.PREVIEW_RESPONSE, data: {preview: "data:123", url: "url"}};
const nextState = TopSites(oldState, action);
assert.propertyVal(nextState.editForm, "previewResponse", "data:123");
});
it("should return previous state if action url does not match expected", () => {
const oldState = {editForm: {previewUrl: "foo"}};
const action = {type: at.PREVIEW_RESPONSE, data: {url: "bar"}};
const nextState = TopSites(oldState, action);
assert.equal(nextState, oldState);
});
it("should return previous state if editForm is not set", () => {
const actionTypes = [at.PREVIEW_RESPONSE, at.PREVIEW_REQUEST, at.PREVIEW_REQUEST_CANCEL];
actionTypes.forEach(type => {
const oldState = {editForm: null};
const action = {type, data: {url: "bar"}};
const nextState = TopSites(oldState, action);
assert.equal(nextState, oldState, type);
});
});
it("should set previewResponse to null on PREVIEW_REQUEST", () => {
const oldState = {editForm: {previewResponse: "foo"}};
const action = {type: at.PREVIEW_REQUEST, data: {}};
const nextState = TopSites(oldState, action);
assert.propertyVal(nextState.editForm, "previewResponse", null);
});
it("should set previewUrl on PREVIEW_REQUEST", () => {
const oldState = {editForm: {}};
const action = {type: at.PREVIEW_REQUEST, data: {url: "bar"}};
const nextState = TopSites(oldState, action);
assert.propertyVal(nextState.editForm, "previewUrl", "bar");
});
it("should add screenshots for SCREENSHOT_UPDATED", () => {
const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
const action = {type: at.SCREENSHOT_UPDATED, data: {url: "bar.com", screenshot: "data:123"}};

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

@ -97,6 +97,7 @@ describe("<TopSites>", () => {
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: {
"custom_screenshot": 0,
"screenshot_with_icon": 0,
"screenshot": 0,
"tippytop": 0,
@ -117,6 +118,7 @@ describe("<TopSites>", () => {
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: {
"custom_screenshot": 0,
"screenshot_with_icon": 0,
"screenshot": 1,
"tippytop": 0,
@ -127,6 +129,27 @@ describe("<TopSites>", () => {
}
}));
});
it("should correctly count TopSite images - custom_screenshot", () => {
const rows = [{customScreenshotURL: true}];
sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
wrapper.instance()._dispatchTopSitesStats();
assert.calledOnce(DEFAULT_PROPS.dispatch);
assert.calledWithExactly(DEFAULT_PROPS.dispatch, ac.AlsoToMain({
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: {
"custom_screenshot": 1,
"screenshot_with_icon": 0,
"screenshot": 0,
"tippytop": 0,
"rich_icon": 0,
"no_image": 0
},
topsites_pinned: 0
}
}));
});
it("should correctly count TopSite images - screenshot + favicon", () => {
const rows = [{screenshot: true, faviconSize: MIN_CORNER_FAVICON_SIZE}];
sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows);
@ -137,6 +160,7 @@ describe("<TopSites>", () => {
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: {
"custom_screenshot": 0,
"screenshot_with_icon": 1,
"screenshot": 0,
"tippytop": 0,
@ -157,6 +181,7 @@ describe("<TopSites>", () => {
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: {
"custom_screenshot": 0,
"screenshot_with_icon": 0,
"screenshot": 0,
"tippytop": 0,
@ -177,6 +202,7 @@ describe("<TopSites>", () => {
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: {
"custom_screenshot": 0,
"screenshot_with_icon": 0,
"screenshot": 0,
"tippytop": 2,
@ -197,6 +223,7 @@ describe("<TopSites>", () => {
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: {
"custom_screenshot": 0,
"screenshot_with_icon": 0,
"screenshot": 0,
"tippytop": 0,
@ -217,6 +244,7 @@ describe("<TopSites>", () => {
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: {
"custom_screenshot": 0,
"screenshot_with_icon": 0,
"screenshot": 0,
"tippytop": 0,
@ -238,6 +266,7 @@ describe("<TopSites>", () => {
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: {
"custom_screenshot": 0,
"screenshot_with_icon": 0,
"screenshot": 0,
"tippytop": 0,
@ -258,6 +287,7 @@ describe("<TopSites>", () => {
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: {
"custom_screenshot": 0,
"screenshot_with_icon": 0,
"screenshot": 0,
"tippytop": 0,
@ -565,6 +595,101 @@ describe("<TopSiteForm>", () => {
assert.isFalse(wrapper.instance().validateForm());
assert.isTrue(wrapper.state().validationError);
});
it("should return true for a correct custom screenshot URL", () => {
wrapper.setState({customScreenshotUrl: "foo"});
assert.isTrue(wrapper.instance().validateForm());
});
it("should return false for a incorrect custom screenshot URL", () => {
wrapper.setState({customScreenshotUrl: " "});
assert.isFalse(wrapper.instance().validateForm());
});
it("should return true for an empty custom screenshot URL", () => {
wrapper.setState({customScreenshotURL: ""});
assert.isTrue(wrapper.instance().validateForm());
});
});
describe("#previewButton", () => {
beforeEach(() => setup({
site: {customScreenshotURL: "http://foo.com"},
previewResponse: null
}));
it("should render the preview button on invalid urls", () => {
assert.equal(0, wrapper.find(".preview").length);
wrapper.setState({customScreenshotUrl: " "});
assert.equal(1, wrapper.find(".preview").length);
});
it("should render the preview button when input value updated", () => {
assert.equal(0, wrapper.find(".preview").length);
wrapper.setState({customScreenshotUrl: "http://baz.com", screenshotPreview: null});
assert.equal(1, wrapper.find(".preview").length);
});
});
describe("preview request", () => {
beforeEach(() => {
setup({
site: {customScreenshotURL: "http://foo.com", url: "http://foo.com"},
previewResponse: null
});
});
it("shouldn't dispatch a request for invalid urls", () => {
wrapper.setState({customScreenshotUrl: " ", url: "foo"});
wrapper.find(".preview").simulate("click");
assert.notCalled(wrapper.props().dispatch);
});
it("should dispatch a PREVIEW_REQUEST", () => {
wrapper.setState({customScreenshotUrl: "screenshot"});
wrapper.find(".preview").simulate("click");
assert.calledTwice(wrapper.props().dispatch);
assert.calledWith(wrapper.props().dispatch, ac.AlsoToMain({
type: at.PREVIEW_REQUEST,
data: {url: "http://screenshot"}
}));
assert.calledWith(wrapper.props().dispatch, ac.UserEvent({
event: "PREVIEW_REQUEST",
source: "TOP_SITES"
}));
});
});
describe("#TopSiteLink", () => {
it("should display a TopSiteLink preview", () => {
assert.equal(wrapper.find(TopSiteLink).length, 1);
});
it("should display the preview screenshot", () => {
wrapper.setProps({site: {tippyTopIcon: "bar"}});
assert.equal(wrapper.find(".top-site-icon").getDOMNode().style["background-image"], "url(\"bar\")");
wrapper.setProps({previewResponse: "foo", previewUrl: "foo"});
assert.equal(wrapper.find(".top-site-icon").getDOMNode().style["background-image"], "url(\"foo\")");
});
it("should not render any icon on error", () => {
wrapper.setProps({previewResponse: ""});
assert.equal(wrapper.find(".top-site-icon").length, 0);
});
});
describe("#addMode", () => {
@ -580,24 +705,23 @@ describe("<TopSiteForm>", () => {
assert.equal(wrapper.findWhere(n => n.length && n.props().id === "topsites_form_save_button").length, 0);
assert.equal(wrapper.findWhere(n => n.length && n.props().id === "topsites_form_add_button").length, 1);
});
it("should not render a preview button", () => {
assert.equal(0, wrapper.find(".custom-image-input-container").length);
});
it("should call onClose if Cancel button is clicked", () => {
wrapper.find(".cancel").simulate("click");
assert.calledOnce(wrapper.instance().props.onClose);
});
it("should show error and not call onClose or dispatch if URL is empty", () => {
assert.isFalse(wrapper.state().validationError);
it("should set validationError if url is empty", () => {
assert.equal(wrapper.state().validationError, false);
wrapper.find(".done").simulate("click");
assert.isTrue(wrapper.state().validationError);
assert.notCalled(wrapper.instance().props.onClose);
assert.notCalled(wrapper.instance().props.dispatch);
assert.equal(wrapper.state().validationError, true);
});
it("should show error and not call onClose or dispatch if URL is invalid", () => {
it("should set validationError if url is invalid", () => {
wrapper.setState({"url": "not valid"});
assert.isFalse(wrapper.state().validationError);
assert.equal(wrapper.state().validationError, false);
wrapper.find(".done").simulate("click");
assert.isTrue(wrapper.state().validationError);
assert.notCalled(wrapper.instance().props.onClose);
assert.notCalled(wrapper.instance().props.dispatch);
assert.equal(wrapper.state().validationError, true);
});
it("should call onClose and dispatch with right args if URL is valid", () => {
wrapper.setState({"url": "valid.com", "label": "a label"});
@ -632,10 +756,17 @@ describe("<TopSiteForm>", () => {
}
);
});
it("should open the custom screenshot input", () => {
assert.isFalse(wrapper.state().showCustomScreenshotForm);
wrapper.find(".enable-custom-image-input").simulate("click");
assert.isTrue(wrapper.state().showCustomScreenshotForm);
});
});
describe("edit existing Topsite", () => {
beforeEach(() => setup({site: {url: "https://foo.bar", label: "baz"}, index: 0}));
beforeEach(() => setup({site: {url: "https://foo.bar", label: "baz", customScreenshotURL: "http://foo"}, index: 7}));
it("should render the component", () => {
assert.ok(wrapper.find(TopSiteForm));
@ -647,29 +778,23 @@ describe("<TopSiteForm>", () => {
assert.equal(wrapper.findWhere(n => n.props().id === "topsites_form_add_button").length, 0);
assert.equal(wrapper.findWhere(n => n.props().id === "topsites_form_save_button").length, 1);
});
it("should have the correct button text (if editing a placeholder)", () => {
wrapper.setProps({site: null});
assert.equal(wrapper.findWhere(n => n.props().id === "topsites_form_save_button").length, 0);
assert.equal(wrapper.findWhere(n => n.props().id === "topsites_form_add_button").length, 1);
});
it("should call onClose if Cancel button is clicked", () => {
wrapper.find(".cancel").simulate("click");
assert.calledOnce(wrapper.instance().props.onClose);
});
it("should show error and not call onClose or dispatch if URL is empty", () => {
wrapper.setState({"url": ""});
assert.isFalse(wrapper.state().validationError);
assert.equal(wrapper.state().validationError, false);
wrapper.find(".done").simulate("click");
assert.isTrue(wrapper.state().validationError);
assert.equal(wrapper.state().validationError, true);
assert.notCalled(wrapper.instance().props.onClose);
assert.notCalled(wrapper.instance().props.dispatch);
});
it("should show error and not call onClose or dispatch if URL is invalid", () => {
wrapper.setState({"url": "not valid"});
assert.isFalse(wrapper.state().validationError);
assert.equal(wrapper.state().validationError, false);
wrapper.find(".done").simulate("click");
assert.isTrue(wrapper.state().validationError);
assert.equal(wrapper.state().validationError, true);
assert.notCalled(wrapper.instance().props.onClose);
assert.notCalled(wrapper.instance().props.dispatch);
});
@ -680,7 +805,7 @@ describe("<TopSiteForm>", () => {
assert.calledWith(
wrapper.instance().props.dispatch,
{
data: {site: {label: "baz", url: "https://foo.bar"}, index: 0},
data: {site: {label: "baz", url: "https://foo.bar", customScreenshotURL: "http://foo"}, index: 7},
meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
type: at.TOP_SITES_PIN
}
@ -688,12 +813,26 @@ describe("<TopSiteForm>", () => {
assert.calledWith(
wrapper.instance().props.dispatch,
{
data: {action_position: 0, source: "TOP_SITES", event: "TOP_SITES_EDIT"},
data: {action_position: 7, source: "TOP_SITES", event: "TOP_SITES_EDIT"},
meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
type: at.TELEMETRY_USER_EVENT
}
);
});
it("should set customScreenshotURL to null if it was removed", () => {
wrapper.setState({customScreenshotUrl: ""});
wrapper.find(".done").simulate("click");
assert.calledWith(
wrapper.instance().props.dispatch,
{
data: {site: {label: "baz", url: "https://foo.bar", customScreenshotURL: null}, index: 7},
meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
type: at.TOP_SITES_PIN
}
);
});
it("should call onClose and dispatch with right args if URL is valid (negative index)", () => {
wrapper.setProps({index: -1});
wrapper.find(".done").simulate("click");
@ -702,7 +841,7 @@ describe("<TopSiteForm>", () => {
assert.calledWith(
wrapper.instance().props.dispatch,
{
data: {site: {label: "baz", url: "https://foo.bar"}, index: -1},
data: {site: {label: "baz", url: "https://foo.bar", customScreenshotURL: "http://foo"}, index: -1},
meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
type: at.TOP_SITES_PIN
}
@ -714,12 +853,45 @@ describe("<TopSiteForm>", () => {
assert.calledWith(
wrapper.instance().props.dispatch,
{
data: {site: {url: "https://foo.bar"}, index: 0},
data: {site: {url: "https://foo.bar", customScreenshotURL: "http://foo"}, index: 7},
meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
type: at.TOP_SITES_PIN
}
);
});
it("should render the save button if custom screenshot request finished", () => {
wrapper.setState({customScreenshotUrl: "foo", screenshotPreview: "custom"});
assert.equal(0, wrapper.find(".preview").length);
assert.equal(1, wrapper.find(".done").length);
});
it("should render the save button if custom screenshot url was cleared", () => {
wrapper.setState({customScreenshotUrl: ""});
wrapper.setProps({site: {customScreenshotURL: "foo"}});
assert.equal(0, wrapper.find(".preview").length);
assert.equal(1, wrapper.find(".done").length);
});
});
describe("#previewMode", () => {
beforeEach(() => setup({previewResponse: null}));
it("should transition from save to preview", () => {
wrapper.setProps({site: {url: "https://foo.bar", customScreenshotURL: "baz"}, index: 7});
assert.equal(wrapper.findWhere(n => n.length && n.props().id === "topsites_form_save_button").length, 1);
wrapper.setState({customScreenshotUrl: "foo"});
assert.equal(wrapper.findWhere(n => n.length && n.props().id === "topsites_form_preview_button").length, 1);
});
it("should transition from add to preview", () => {
assert.equal(wrapper.findWhere(n => n.length && n.props().id === "topsites_form_add_button").length, 1);
wrapper.setState({customScreenshotUrl: "foo"});
assert.equal(wrapper.findWhere(n => n.length && n.props().id === "topsites_form_preview_button").length, 1);
});
});
describe("#validateUrl", () => {
@ -815,14 +987,14 @@ describe("<TopSiteList>", () => {
const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} dispatch={dispatch} />);
const instance = wrapper.instance();
const index = 7;
const link = {url: "https://foo.com"};
const link = {url: "https://foo.com", customScreenshotURL: "foo"};
const title = "foo";
instance.onDragEvent({type: "dragstart"}, index, link, title);
dispatch.reset();
instance.onDragEvent({type: "drop"}, 3);
assert.calledTwice(dispatch);
assert.calledWith(dispatch, {
data: {draggedFromIndex: 7, index: 3, site: {label: "foo", url: "https://foo.com"}},
data: {draggedFromIndex: 7, index: 3, site: {label: "foo", url: "https://foo.com", customScreenshotURL: "foo"}},
meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
type: "TOP_SITES_INSERT"
});
@ -939,6 +1111,21 @@ describe("#TopSiteFormInput", () => {
assert.equal(wrapper.find(".icon-clear-input").length, 1);
});
it("should show the loading indicator", () => {
assert.equal(wrapper.find(".loading-container").length, 0);
wrapper.setProps({loading: true});
assert.equal(wrapper.find(".loading-container").length, 1);
});
it("should disable the input when loading indicator is present", () => {
assert.isFalse(wrapper.find("input").getDOMNode().disabled);
wrapper.setProps({loading: true});
assert.isTrue(wrapper.find("input").getDOMNode().disabled);
});
});
describe("with error", () => {
@ -956,5 +1143,11 @@ describe("#TopSiteFormInput", () => {
it("should render the error message", () => {
assert.equal(wrapper.findWhere(n => n.props().id === "topsites_form_url_validation").length, 1);
});
it("should reset the error state on value change", () => {
wrapper.find("input").simulate("change", {target: {value: "bar"}});
assert.isFalse(wrapper.state().validationError);
});
});
});

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

@ -194,6 +194,7 @@ describe("TelemetryFeed", () => {
const session = instance.addSession(portID, "about:home");
instance.saveSessionPerfData("foo", {
topsites_icon_stats: {
"custom_screenshot": 0,
"screenshot_with_icon": 2,
"screenshot": 1,
"tippytop": 2,

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

@ -61,7 +61,7 @@ describe("Top Sites Feed", () => {
};
fakeScreenshot = {
getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT)),
maybeCacheScreenshot: Screenshots.maybeCacheScreenshot,
maybeCacheScreenshot: sandbox.spy(Screenshots.maybeCacheScreenshot),
_shouldGetScreenshots: sinon.stub().returns(true)
};
filterAdultStub = sinon.stub().returns([]);
@ -145,14 +145,14 @@ describe("Top Sites Feed", () => {
});
describe("#filterForThumbnailExpiration", () => {
it("should pass rows.urls to the callback provided", () => {
const rows = [{url: "foo.com"}, {"url": "bar.com"}];
const rows = [{url: "foo.com"}, {"url": "bar.com", "customScreenshotURL": "custom"}];
feed.store.state.TopSites = {rows};
const stub = sinon.stub();
feed.filterForThumbnailExpiration(stub);
assert.calledOnce(stub);
assert.calledWithExactly(stub, rows.map(r => r.url));
assert.calledWithExactly(stub, ["foo.com", "bar.com", "custom"]);
});
});
describe("#getLinksWithDefaults", () => {
@ -161,9 +161,6 @@ describe("Top Sites Feed", () => {
});
describe("general", () => {
beforeEach(() => {
sandbox.stub(fakeScreenshot, "maybeCacheScreenshot");
});
it("should get the links from NewTabUtils", async () => {
const result = await feed.getLinksWithDefaults();
const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
@ -402,6 +399,15 @@ describe("Top Sites Feed", () => {
assert.calledWith(feed._fetchIcon, link);
});
});
it("should call _fetchScreenshot when customScreenshotURL is set", async () => {
links = [];
fakeNewTabUtils.pinnedLinks.links = [{url: "foo", customScreenshotURL: "custom"}];
sinon.stub(feed, "_fetchScreenshot");
await feed.getLinksWithDefaults();
assert.calledWith(feed._fetchScreenshot, sinon.match.object, "custom");
});
});
describe("#refresh", () => {
beforeEach(() => {
@ -466,6 +472,27 @@ describe("Top Sites Feed", () => {
}));
});
});
describe("#getScreenshotPreview", () => {
it("should dispatch preview if request is succesful", async () => {
await feed.getScreenshotPreview("custom", 1234);
assert.calledOnce(feed.store.dispatch);
assert.calledWithExactly(feed.store.dispatch, ac.OnlyToOneContent({
data: {preview: FAKE_SCREENSHOT, url: "custom"},
type: at.PREVIEW_RESPONSE
}, 1234));
});
it("should return empty string if request fails", async () => {
fakeScreenshot.getScreenshotForURL = sandbox.stub().returns(Promise.resolve(null));
await feed.getScreenshotPreview("custom", 1234);
assert.calledOnce(feed.store.dispatch);
assert.calledWithExactly(feed.store.dispatch, ac.OnlyToOneContent({
data: {preview: "", url: "custom"},
type: at.PREVIEW_RESPONSE
}, 1234));
});
});
describe("#_fetchIcon", () => {
it("should reuse screenshot on the link", () => {
const link = {screenshot: "reuse.png"};
@ -535,7 +562,33 @@ describe("Top Sites Feed", () => {
assert.notProperty(link, "tippyTopIcon");
});
});
describe("#_fetchScreenshot", () => {
it("should call maybeCacheScreenshot", async () => {
const updateLink = sinon.stub();
const link = {customScreenshotURL: "custom", __sharedCache: {updateLink}};
await feed._fetchScreenshot(link, "custom");
assert.calledOnce(fakeScreenshot.maybeCacheScreenshot);
assert.calledWithExactly(fakeScreenshot.maybeCacheScreenshot, link, link.customScreenshotURL,
"screenshot", sinon.match.func);
});
it("should not call maybeCacheScreenshot if screenshot is set", async () => {
const updateLink = sinon.stub();
const link = {customScreenshotURL: "custom", __sharedCache: {updateLink}, screenshot: true};
await feed._fetchScreenshot(link, "custom");
assert.notCalled(fakeScreenshot.maybeCacheScreenshot);
});
});
describe("#onAction", () => {
it("should call getScreenshotPreview on PREVIEW_REQUEST", () => {
sandbox.stub(feed, "getScreenshotPreview");
feed.onAction({type: at.PREVIEW_REQUEST, data: {url: "foo"}, meta: {fromTarget: 1234}});
assert.calledOnce(feed.getScreenshotPreview);
assert.calledWithExactly(feed.getScreenshotPreview, "foo", 1234);
});
it("should refresh on SYSTEM_TICK", async () => {
sandbox.stub(feed, "refresh");
@ -553,20 +606,36 @@ describe("Top Sites Feed", () => {
assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, pinAction.data.site, pinAction.data.index);
});
it("should trigger refresh on TOP_SITES_PIN", () => {
sinon.stub(feed, "refresh");
it("should call pin on TOP_SITES_PIN", () => {
sinon.stub(feed, "pin");
const pinExistingAction = {type: at.TOP_SITES_PIN, data: {site: FAKE_LINKS[4], index: 4}};
feed.onAction(pinExistingAction);
assert.calledOnce(feed.pin);
});
it("should trigger refresh on TOP_SITES_PIN", async () => {
sinon.stub(feed, "refresh");
const pinExistingAction = {type: at.TOP_SITES_PIN, data: {site: FAKE_LINKS[4], index: 4}};
await feed.pin(pinExistingAction);
assert.calledOnce(feed.refresh);
});
it("should trigger refresh on TOP_SITES_INSERT", () => {
sinon.stub(feed, "refresh");
it("should call insert on TOP_SITES_INSERT", async () => {
sinon.stub(feed, "insert");
const addAction = {type: at.TOP_SITES_INSERT, data: {site: {url: "foo.com"}}};
feed.onAction(addAction);
assert.calledOnce(feed.insert);
});
it("should trigger refresh on TOP_SITES_INSERT", async () => {
sinon.stub(feed, "refresh");
const addAction = {type: at.TOP_SITES_INSERT, data: {site: {url: "foo.com"}}};
await feed.insert(addAction);
assert.calledOnce(feed.refresh);
});
it("should call unpin with correct parameters on TOP_SITES_UNPIN", () => {
@ -700,12 +769,36 @@ describe("Top Sites Feed", () => {
});
});
describe("#pin", () => {
it("should pin site in specified slot empty pinned list", () => {
const site = {url: "foo.bar", label: "foo"};
feed.pin({data: {index: 2, site}});
it("should pin site in specified slot empty pinned list", async () => {
const site = {url: "foo.bar", label: "foo", customScreenshotURL: "screenshot"};
await feed.pin({data: {index: 2, site}});
assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
});
it("should lookup the link object to update the custom screenshot", async () => {
const site = {url: "foo.bar", label: "foo", customScreenshotURL: "screenshot"};
sandbox.spy(feed.pinnedCache, "request");
await feed.pin({data: {index: 2, site}});
assert.calledOnce(feed.pinnedCache.request);
});
it("should lookup the link object to update the custom screenshot", async () => {
const site = {url: "foo.bar", label: "foo", customScreenshotURL: null};
sandbox.spy(feed.pinnedCache, "request");
await feed.pin({data: {index: 2, site}});
assert.calledOnce(feed.pinnedCache.request);
});
it("should not do a link object lookup if custom screenshot field is not set", async () => {
const site = {url: "foo.bar", label: "foo"};
sandbox.spy(feed.pinnedCache, "request");
await feed.pin({data: {index: 2, site}});
assert.notCalled(feed.pinnedCache.request);
});
it("should pin site in specified slot of pinned list that is free", () => {
fakeNewTabUtils.pinnedLinks.links = [null, {url: "example.com"}];
const site = {url: "foo.bar", label: "foo"};
@ -758,6 +851,34 @@ describe("Top Sites Feed", () => {
assert.notCalled(feed.insert);
});
});
describe("clearLinkCustomScreenshot", () => {
it("should remove cached screenshot if custom url changes", async () => {
const stub = sandbox.stub();
sandbox.stub(feed.pinnedCache, "request").returns(Promise.resolve([{
url: "foo",
customScreenshotURL: "old_screenshot",
__sharedCache: {updateLink: stub}
}]));
await feed._clearLinkCustomScreenshot({url: "foo", customScreenshotURL: "new_screenshot"});
assert.calledOnce(stub);
assert.calledWithExactly(stub, "screenshot", undefined);
});
it("should remove cached screenshot if custom url is removed", async () => {
const stub = sandbox.stub();
sandbox.stub(feed.pinnedCache, "request").returns(Promise.resolve([{
url: "foo",
customScreenshotURL: "old_screenshot",
__sharedCache: {updateLink: stub}
}]));
await feed._clearLinkCustomScreenshot({url: "foo", customScreenshotURL: "new_screenshot"});
assert.calledOnce(stub);
assert.calledWithExactly(stub, "screenshot", undefined);
});
});
describe("#drop", () => {
it("should correctly handle different index values", () => {
let index = -1;