feat(highlights): Lazily add DownloadsManager view to handle recent downloads (#4076)
Fix Bug 1433230 - Add recent downloads to Highlights
This commit is contained in:
Родитель
86050a1c4d
Коммит
e87a190e15
|
@ -22,6 +22,7 @@ type_label_visited=Visited
|
|||
type_label_bookmarked=Bookmarked
|
||||
type_label_recommended=Trending
|
||||
type_label_pocket=Saved to Pocket
|
||||
type_label_downloaded=Downloaded
|
||||
|
||||
# LOCALIZATION NOTE (menu_action_*): These strings are displayed in a context
|
||||
# menu and are meant as a call to action for a given page.
|
||||
|
@ -44,6 +45,23 @@ menu_action_save_to_pocket=Save to Pocket
|
|||
menu_action_delete_pocket=Delete from Pocket
|
||||
menu_action_archive_pocket=Archive in Pocket
|
||||
|
||||
# LOCALIZATION NOTE (menu_action_show_file_*): These are platform specific strings
|
||||
# found in the context menu of an item that has been downloaded. The intention behind
|
||||
# "this action" is that it will show where the downloaded file exists on the file system
|
||||
# for each operating system.
|
||||
menu_action_show_file_mac_os=Show in Finder
|
||||
menu_action_show_file_windows=Open Containing Folder
|
||||
menu_action_show_file_linux=Open Containing Folder
|
||||
menu_action_show_file_default=Show File
|
||||
menu_action_open_file=Open File
|
||||
|
||||
# LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
|
||||
# "Download" here, in both cases, is not a verb, it is a noun. As in, "Copy the
|
||||
# link that belongs to this downloaded item"
|
||||
menu_action_copy_download_link=Copy Download Link
|
||||
menu_action_go_to_download_page=Go to Download Page
|
||||
menu_action_remove_download=Remove from History
|
||||
|
||||
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
|
||||
# search button.
|
||||
search_button=Search
|
||||
|
|
|
@ -28,12 +28,15 @@ for (const type of [
|
|||
"ARCHIVE_FROM_POCKET",
|
||||
"BLOCK_URL",
|
||||
"BOOKMARK_URL",
|
||||
"COPY_DOWNLOAD_LINK",
|
||||
"DELETE_BOOKMARK_BY_ID",
|
||||
"DELETE_FROM_POCKET",
|
||||
"DELETE_HISTORY_URL",
|
||||
"DIALOG_CANCEL",
|
||||
"DIALOG_OPEN",
|
||||
"DISABLE_ONBOARDING",
|
||||
"DOWNLOAD_CHANGED",
|
||||
"GO_TO_DOWNLOAD_PAGE",
|
||||
"INIT",
|
||||
"MIGRATION_CANCEL",
|
||||
"MIGRATION_COMPLETED",
|
||||
|
@ -44,6 +47,7 @@ for (const type of [
|
|||
"NEW_TAB_REHYDRATED",
|
||||
"NEW_TAB_STATE_REQUEST",
|
||||
"NEW_TAB_UNLOAD",
|
||||
"OPEN_DOWNLOAD_FILE",
|
||||
"OPEN_LINK",
|
||||
"OPEN_NEW_WINDOW",
|
||||
"OPEN_PRIVATE_WINDOW",
|
||||
|
@ -60,6 +64,7 @@ for (const type of [
|
|||
"PREVIEW_REQUEST",
|
||||
"PREVIEW_REQUEST_CANCEL",
|
||||
"PREVIEW_RESPONSE",
|
||||
"REMOVE_DOWNLOAD_FILE",
|
||||
"RICH_ICON_MISSING",
|
||||
"SAVE_SESSION_PERF_DATA",
|
||||
"SAVE_TO_POCKET",
|
||||
|
@ -75,6 +80,7 @@ for (const type of [
|
|||
"SETTINGS_CLOSE",
|
||||
"SETTINGS_OPEN",
|
||||
"SET_PREF",
|
||||
"SHOW_DOWNLOAD_FILE",
|
||||
"SHOW_FIREFOX_ACCOUNTS",
|
||||
"SNIPPETS_BLOCKLIST_CLEARED",
|
||||
"SNIPPETS_BLOCKLIST_UPDATED",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
|
||||
import {cardContextTypes} from "./types";
|
||||
import {connect} from "react-redux";
|
||||
import {FormattedMessage} from "react-intl";
|
||||
import {GetPlatformString} from "content-src/lib/link-menu-options";
|
||||
import {LinkMenu} from "content-src/components/LinkMenu/LinkMenu";
|
||||
import React from "react";
|
||||
|
||||
|
@ -16,7 +18,7 @@ const gImageLoading = new Map();
|
|||
* this class. Each card will then get a context menu which reflects the actions that
|
||||
* can be done on this Card.
|
||||
*/
|
||||
export class Card extends React.PureComponent {
|
||||
export class _Card extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -82,12 +84,18 @@ export class Card extends React.PureComponent {
|
|||
|
||||
onLinkClick(event) {
|
||||
event.preventDefault();
|
||||
const {altKey, button, ctrlKey, metaKey, shiftKey} = event;
|
||||
this.props.dispatch(ac.AlsoToMain({
|
||||
type: at.OPEN_LINK,
|
||||
data: Object.assign(this.props.link, {event: {altKey, button, ctrlKey, metaKey, shiftKey}})
|
||||
}));
|
||||
|
||||
if (this.props.link.type === "download") {
|
||||
this.props.dispatch(ac.OnlyToMain({
|
||||
type: at.SHOW_DOWNLOAD_FILE,
|
||||
data: this.props.link
|
||||
}));
|
||||
} else {
|
||||
const {altKey, button, ctrlKey, metaKey, shiftKey} = event;
|
||||
this.props.dispatch(ac.OnlyToMain({
|
||||
type: at.OPEN_LINK,
|
||||
data: Object.assign(this.props.link, {event: {altKey, button, ctrlKey, metaKey, shiftKey}})
|
||||
}));
|
||||
}
|
||||
if (this.props.isWebExtension) {
|
||||
this.props.dispatch(ac.WebExtEvent(at.WEBEXT_CLICK, {
|
||||
source: this.props.eventSource,
|
||||
|
@ -146,6 +154,8 @@ export class Card extends React.PureComponent {
|
|||
<div className={`card-preview-image${this.state.imageLoaded ? " loaded" : ""}`} style={imageStyle} />
|
||||
</div>}
|
||||
<div className={`card-details${hasImage ? "" : " no-image"}`}>
|
||||
{link.type === "download" && <div className="card-download-icon icon icon-download-folder" />}
|
||||
{link.type === "download" && <div className="card-host-name alternate"><FormattedMessage id={GetPlatformString(this.props.platform)} /></div>}
|
||||
{link.hostname && <div className="card-host-name">{link.hostname}</div>}
|
||||
<div className={[
|
||||
"card-text",
|
||||
|
@ -184,6 +194,6 @@ export class Card extends React.PureComponent {
|
|||
</li>);
|
||||
}
|
||||
}
|
||||
Card.defaultProps = {link: {}};
|
||||
|
||||
_Card.defaultProps = {link: {}};
|
||||
export const Card = connect(state => ({platform: state.Prefs.values.platform}))(_Card);
|
||||
export const PlaceholderCard = () => <Card placeholder={true} />;
|
||||
|
|
|
@ -49,6 +49,14 @@
|
|||
.card-title {
|
||||
color: var(--newtab-link-primary-color);
|
||||
}
|
||||
|
||||
.alternate ~ .card-host-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card-host-name.alternate {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.card-preview-image-outer {
|
||||
|
@ -129,6 +137,19 @@
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.card-host-name.alternate { display: none; }
|
||||
|
||||
.card-download-icon {
|
||||
float: inline-end;
|
||||
margin-inline-start: 15px;
|
||||
margin-top: 2px;
|
||||
|
||||
&.icon-download-folder {
|
||||
height: $small-download-folder-icon-size;
|
||||
width: $small-download-folder-icon-size;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
@ -195,6 +216,13 @@
|
|||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-download-icon {
|
||||
&.icon-download-folder {
|
||||
height: $large-download-folder-icon-size;
|
||||
width: $large-download-folder-icon-size;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 17px;
|
||||
line-height: $line-height;
|
||||
|
|
|
@ -18,5 +18,9 @@ export const cardContextTypes = {
|
|||
pocket: {
|
||||
intlID: "type_label_pocket",
|
||||
icon: "pocket"
|
||||
},
|
||||
download: {
|
||||
intlID: "type_label_downloaded",
|
||||
icon: "download"
|
||||
}
|
||||
};
|
||||
|
|
|
@ -10,12 +10,12 @@ const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator"
|
|||
export class _LinkMenu extends React.PureComponent {
|
||||
getOptions() {
|
||||
const {props} = this;
|
||||
const {site, index, source, isPrivateBrowsingEnabled, siteInfo} = props;
|
||||
const {site, index, source, isPrivateBrowsingEnabled, siteInfo, platform} = props;
|
||||
|
||||
// Handle special case of default site
|
||||
const propOptions = !site.isDefault ? props.options : DEFAULT_SITE_MENU_OPTIONS;
|
||||
|
||||
const options = propOptions.map(o => LinkMenuOptions[o](site, index, source, isPrivateBrowsingEnabled, siteInfo)).map(option => {
|
||||
const options = propOptions.map(o => LinkMenuOptions[o](site, index, source, isPrivateBrowsingEnabled, siteInfo, platform)).map(option => {
|
||||
const {action, impression, id, string_id, type, userEvent} = option;
|
||||
if (!type && id) {
|
||||
option.label = props.intl.formatMessage({id: string_id || id});
|
||||
|
@ -52,5 +52,5 @@ export class _LinkMenu extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const getState = state => ({isPrivateBrowsingEnabled: state.Prefs.values.isPrivateBrowsingEnabled});
|
||||
const getState = state => ({isPrivateBrowsingEnabled: state.Prefs.values.isPrivateBrowsingEnabled, platform: state.Prefs.values.platform});
|
||||
export const LinkMenu = connect(getState)(injectIntl(_LinkMenu));
|
||||
|
|
|
@ -10,6 +10,19 @@ const _OpenInPrivateWindow = site => ({
|
|||
userEvent: "OPEN_PRIVATE_WINDOW"
|
||||
});
|
||||
|
||||
export const GetPlatformString = platform => {
|
||||
switch (platform) {
|
||||
case "win":
|
||||
return "menu_action_show_file_windows";
|
||||
case "macosx":
|
||||
return "menu_action_show_file_mac_os";
|
||||
case "linux":
|
||||
return "menu_action_show_file_linux";
|
||||
default:
|
||||
return "menu_action_show_file_default";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* List of functions that return items that can be included as menu options in a
|
||||
* LinkMenu. All functions take the site as the first parameter, and optionally
|
||||
|
@ -91,6 +104,47 @@ export const LinkMenuOptions = {
|
|||
},
|
||||
userEvent: "DIALOG_OPEN"
|
||||
}),
|
||||
ShowFile: (site, index, eventSource, isEnabled, siteInfo, platform) => ({
|
||||
id: GetPlatformString(platform),
|
||||
icon: "search",
|
||||
action: ac.OnlyToMain({
|
||||
type: at.SHOW_DOWNLOAD_FILE,
|
||||
data: {url: site.url}
|
||||
})
|
||||
}),
|
||||
OpenFile: site => ({
|
||||
id: "menu_action_open_file",
|
||||
icon: "open-file",
|
||||
action: ac.OnlyToMain({
|
||||
type: at.OPEN_DOWNLOAD_FILE,
|
||||
data: {url: site.url}
|
||||
})
|
||||
}),
|
||||
CopyDownloadLink: site => ({
|
||||
id: "menu_action_copy_download_link",
|
||||
icon: "copy",
|
||||
action: ac.OnlyToMain({
|
||||
type: at.COPY_DOWNLOAD_LINK,
|
||||
data: {url: site.url}
|
||||
})
|
||||
}),
|
||||
GoToDownloadPage: site => ({
|
||||
id: "menu_action_go_to_download_page",
|
||||
icon: "download",
|
||||
action: ac.OnlyToMain({
|
||||
type: at.GO_TO_DOWNLOAD_PAGE,
|
||||
data: {url: site.url}
|
||||
}),
|
||||
disabled: !site.referrer
|
||||
}),
|
||||
RemoveDownload: site => ({
|
||||
id: "menu_action_remove_download",
|
||||
icon: "delete",
|
||||
action: ac.OnlyToMain({
|
||||
type: at.REMOVE_DOWNLOAD_FILE,
|
||||
data: {url: site.url}
|
||||
})
|
||||
}),
|
||||
PinTopSite: (site, index) => ({
|
||||
id: "menu_action_pin",
|
||||
icon: "pin",
|
||||
|
|
|
@ -34,6 +34,10 @@
|
|||
background-image: url('#{$image-path}glyph-delete-16.svg');
|
||||
}
|
||||
|
||||
&.icon-search {
|
||||
background-image: url('chrome://browser/skin/search-glass.svg');
|
||||
}
|
||||
|
||||
&.icon-modal-delete {
|
||||
background-image: url('#{$image-path}glyph-modal-delete-32.svg');
|
||||
background-size: $larger-icon-size;
|
||||
|
@ -113,6 +117,23 @@
|
|||
background-image: url('chrome://browser/skin/check.svg');
|
||||
}
|
||||
|
||||
&.icon-download {
|
||||
background-image: url('chrome://browser/skin/downloads/download-icons.svg#arrow-with-bar');
|
||||
}
|
||||
|
||||
&.icon-copy {
|
||||
background-image: url('chrome://browser/skin/edit-copy.svg');
|
||||
}
|
||||
|
||||
&.icon-open-file {
|
||||
background-image: url('#{$image-path}glyph-open-file-16.svg');
|
||||
}
|
||||
|
||||
&.icon-download-folder {
|
||||
background-image: url('#{$image-path}glyph-download-icon.svg');
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
&.icon-webextension {
|
||||
background-image: url('#{$image-path}glyph-webextension-16.svg');
|
||||
}
|
||||
|
|
|
@ -64,6 +64,9 @@ $icon-size: 16px;
|
|||
$smaller-icon-size: 12px;
|
||||
$larger-icon-size: 32px;
|
||||
|
||||
$small-download-folder-icon-size: 36px;
|
||||
$large-download-folder-icon-size: $small-download-folder-icon-size * 1.5;
|
||||
|
||||
$wrapper-default-width: $grid-unit * 2 + $base-gutter * 1 + $section-horizontal-padding * 2; // 2 top sites
|
||||
$wrapper-max-width-small: $grid-unit * 3 + $base-gutter * 2 + $section-horizontal-padding * 2; // 3 top sites
|
||||
$wrapper-max-width-medium: $grid-unit * 4 + $base-gutter * 3 + $section-horizontal-padding * 2; // 4 top sites
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg width="42" height="42" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M27 16h-5.849l-1.567-1.462A2 2 0 0 0 18.219 14H15a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2zm-8.781 0l1.072 1H15v-1h3.219zM27 26H15v-8h6v-.014c.05 0 .1.014.151.014H27v8z" id="a"/></defs><g fill="none" fill-rule="evenodd"><rect fill="#0A84FF" width="42" height="42" rx="21"/><use fill="#FFF" fill-rule="nonzero" xlink:href="#a"/></g></svg>
|
После Ширина: | Высота: | Размер: 482 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="context-fill" d="M14.859 3.2a1.335 1.335 0 0 1-1.217.8H13v1h1v8H2V5h8V4h-.642a1.365 1.365 0 0 1-1.325-1.11L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-1.141-1.8zM2 3h3.219l1.072 1H2zm7.854-.146L11 1.707V8.5a.5.5 0 0 0 1 0V1.707l1.146 1.146a.5.5 0 1 0 .707-.707l-2-2a.5.5 0 0 0-.707 0l-2 2a.5.5 0 0 0 .707.707z"/></svg>
|
После Ширина: | Высота: | Размер: 441 B |
|
@ -128,6 +128,10 @@ const PREFS_CONFIG = new Map([
|
|||
title: "Boolean flag that decides whether or not to show saved Pocket stories in highlights.",
|
||||
value: true
|
||||
}],
|
||||
["section.highlights.includeDownloads", {
|
||||
title: "Boolean flag that decides whether or not to show saved recent Downloads in highlights.",
|
||||
value: true
|
||||
}],
|
||||
["section.topstories.showDisclaimer", {
|
||||
title: "Boolean flag that decides whether or not to show the topstories disclaimer.",
|
||||
value: true
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
Cu.importGlobalProperties(["URL"]);
|
||||
|
||||
const {actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "DownloadsViewUI",
|
||||
"resource:///modules/DownloadsViewUI.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "DownloadsCommon",
|
||||
"resource:///modules/DownloadsCommon.jsm");
|
||||
|
||||
const DOWNLOAD_CHANGED_DELAY_TIME = 1000; // time in ms to delay timer for downloads changed events
|
||||
|
||||
class DownloadElement extends DownloadsViewUI.DownloadElementShell {
|
||||
constructor(download, browser) {
|
||||
super();
|
||||
this._download = download;
|
||||
this.element = browser;
|
||||
this.element._shell = this;
|
||||
}
|
||||
|
||||
get download() {
|
||||
return this._download;
|
||||
}
|
||||
|
||||
get fileType() {
|
||||
if (!this.download.target.path) {
|
||||
return null;
|
||||
}
|
||||
let items = this.download.target.path.split(".");
|
||||
return items[items.length - 1].toUpperCase();
|
||||
}
|
||||
|
||||
downloadsCmd_copyLocation() {
|
||||
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
|
||||
clipboard.copyString(this.download.source.url);
|
||||
}
|
||||
|
||||
downloadsCmd_openReferrer() {
|
||||
this.element.openNewTabWith(this.download.source.referrer, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.DownloadsManager = class DownloadsManager {
|
||||
constructor(store) {
|
||||
this._downloadData = null;
|
||||
this._store = null;
|
||||
this._viewableDownloadItems = new Map();
|
||||
this._downloadTimer = null;
|
||||
}
|
||||
|
||||
setTimeout(callback, delay) {
|
||||
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
timer.initWithCallback(callback, delay, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
return timer;
|
||||
}
|
||||
|
||||
formatDownload(element) {
|
||||
const downloadedItem = element.download;
|
||||
let description;
|
||||
if (element.fileType) {
|
||||
// If we have a file type: '1.5 MB — PNG'
|
||||
description = `${element.sizeStrings.stateLabel} \u2014 ${element.fileType}`;
|
||||
} else {
|
||||
// If we do not have a file type: '1.5 MB'
|
||||
description = `${element.sizeStrings.stateLabel}`;
|
||||
}
|
||||
return {
|
||||
hostname: new URL(downloadedItem.source.url).hostname,
|
||||
url: downloadedItem.source.url,
|
||||
path: downloadedItem.target.path,
|
||||
title: element.displayName,
|
||||
description,
|
||||
referrer: downloadedItem.source.referrer
|
||||
};
|
||||
}
|
||||
|
||||
init(store) {
|
||||
this._store = store;
|
||||
this._browser = Services.appShell.hiddenDOMWindow;
|
||||
this._downloadData = DownloadsCommon.getData(this._browser.ownerGlobal, true, false, true);
|
||||
this._downloadData.addView(this);
|
||||
}
|
||||
|
||||
onDownloadAdded(download) {
|
||||
const elem = new DownloadElement(download, this._browser);
|
||||
const downloadedItem = elem.download;
|
||||
if (!this._viewableDownloadItems.has(downloadedItem.source.url)) {
|
||||
this._viewableDownloadItems.set(downloadedItem.source.url, elem);
|
||||
|
||||
// On startup, all existing downloads fire this notification, so debounce them
|
||||
if (this._downloadTimer) {
|
||||
this._downloadTimer.delay = DOWNLOAD_CHANGED_DELAY_TIME;
|
||||
} else {
|
||||
this._downloadTimer = this.setTimeout(() => {
|
||||
this._downloadTimer = null;
|
||||
this._store.dispatch({type: at.DOWNLOAD_CHANGED});
|
||||
}, DOWNLOAD_CHANGED_DELAY_TIME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDownloadRemoved(download) {
|
||||
if (this._viewableDownloadItems.has(download.source.url)) {
|
||||
this._viewableDownloadItems.delete(download.source.url);
|
||||
this._store.dispatch({type: at.DOWNLOAD_CHANGED});
|
||||
}
|
||||
}
|
||||
|
||||
async getDownloads(threshold, {numItems = this._viewableDownloadItems.size, onlySucceeded = false, onlyExists = false}) {
|
||||
if (!threshold) {
|
||||
return [];
|
||||
}
|
||||
let results = [];
|
||||
|
||||
// Only get downloads within the time threshold specified and sort by recency
|
||||
const downloadThreshold = Date.now() - threshold;
|
||||
let downloads = [...this._viewableDownloadItems.values()]
|
||||
.filter(elem => elem.download.endTime > downloadThreshold)
|
||||
.sort((elem1, elem2) => elem1.download.endTime < elem2.download.endTime);
|
||||
|
||||
for (const elem of downloads) {
|
||||
// Only include downloads where the file still exists
|
||||
if (onlyExists) {
|
||||
// Refresh download to ensure the 'exists' attribute is up to date
|
||||
await elem.download.refresh();
|
||||
if (!elem.download.target.exists) { continue; }
|
||||
}
|
||||
// Only include downloads that were completed successfully
|
||||
if (onlySucceeded) {
|
||||
if (!elem.download.succeeded) { continue; }
|
||||
}
|
||||
const formattedDownloadForHighlights = this.formatDownload(elem);
|
||||
results.push(formattedDownloadForHighlights);
|
||||
if (results.length === numItems) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
uninit() {
|
||||
if (this._downloadData) {
|
||||
this._downloadData.removeView(this);
|
||||
this._downloadData = null;
|
||||
}
|
||||
if (this._downloadTimer) {
|
||||
this._downloadTimer.cancel();
|
||||
this._downloadTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onAction(action) {
|
||||
let downloadsCmd;
|
||||
switch (action.type) {
|
||||
case at.COPY_DOWNLOAD_LINK:
|
||||
downloadsCmd = "downloadsCmd_copyLocation";
|
||||
break;
|
||||
case at.GO_TO_DOWNLOAD_PAGE:
|
||||
downloadsCmd = "downloadsCmd_openReferrer";
|
||||
break;
|
||||
case at.REMOVE_DOWNLOAD_FILE:
|
||||
downloadsCmd = "downloadsCmd_delete";
|
||||
break;
|
||||
case at.SHOW_DOWNLOAD_FILE:
|
||||
downloadsCmd = "downloadsCmd_show";
|
||||
break;
|
||||
case at.OPEN_DOWNLOAD_FILE:
|
||||
downloadsCmd = "downloadsCmd_open";
|
||||
break;
|
||||
case at.UNINIT:
|
||||
this.uninit();
|
||||
break;
|
||||
}
|
||||
// Call the appropriate downloads command function based on the event we received
|
||||
if (downloadsCmd) {
|
||||
let elem = this._viewableDownloadItems.get(action.data.url);
|
||||
if (elem) {
|
||||
elem[downloadsCmd]();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
this.EXPORTED_SYMBOLS = ["DownloadsManager"];
|
|
@ -22,6 +22,8 @@ ChromeUtils.defineModuleGetter(this, "Screenshots",
|
|||
"resource://activity-stream/lib/Screenshots.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "PageThumbs",
|
||||
"resource://gre/modules/PageThumbs.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "DownloadsManager",
|
||||
"resource://activity-stream/lib/DownloadsManager.jsm");
|
||||
|
||||
const HIGHLIGHTS_MAX_LENGTH = 9;
|
||||
const MANY_EXTRA_LENGTH = HIGHLIGHTS_MAX_LENGTH * 5 + TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW;
|
||||
|
@ -29,6 +31,7 @@ const SECTION_ID = "highlights";
|
|||
const SYNC_BOOKMARKS_FINISHED_EVENT = "weave:engine:sync:applied";
|
||||
const BOOKMARKS_RESTORE_SUCCESS_EVENT = "bookmarks-restore-success";
|
||||
const BOOKMARKS_RESTORE_FAILED_EVENT = "bookmarks-restore-failed";
|
||||
const RECENT_DOWNLOAD_THRESHOLD = 36 * 60 * 60 * 1000;
|
||||
|
||||
this.HighlightsFeed = class HighlightsFeed {
|
||||
constructor() {
|
||||
|
@ -36,11 +39,12 @@ this.HighlightsFeed = class HighlightsFeed {
|
|||
this.linksCache = new LinksCache(NewTabUtils.activityStreamLinks,
|
||||
"getHighlights", ["image"]);
|
||||
PageThumbs.addExpirationFilter(this);
|
||||
this.downloadsManager = new DownloadsManager();
|
||||
}
|
||||
|
||||
_dedupeKey(site) {
|
||||
// Treat bookmarks and pocket items as un-dedupable, otherwise show one of a url
|
||||
return site && ((site.pocket_id || site.type === "bookmark") ? {} : site.url);
|
||||
// Treat bookmarks, pocket, and downloaded items as un-dedupable, otherwise show one of a url
|
||||
return site && ((site.pocket_id || site.type === "bookmark" || site.type === "download") ? {} : site.url);
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -53,6 +57,7 @@ this.HighlightsFeed = class HighlightsFeed {
|
|||
postInit() {
|
||||
SectionsManager.enableSection(SECTION_ID);
|
||||
this.fetchHighlights({broadcast: true});
|
||||
this.downloadsManager.init(this.store);
|
||||
}
|
||||
|
||||
uninit() {
|
||||
|
@ -137,6 +142,16 @@ this.HighlightsFeed = class HighlightsFeed {
|
|||
excludePocket: !this.store.getState().Prefs.values["section.highlights.includePocket"]
|
||||
});
|
||||
|
||||
if (this.store.getState().Prefs.values["section.highlights.includeDownloads"]) {
|
||||
// We only want 1 download that is less than 36 hours old, and the file currently exists
|
||||
let results = await this.downloadsManager.getDownloads(RECENT_DOWNLOAD_THRESHOLD, {numItems: 1, onlySucceeded: true, onlyExists: true});
|
||||
if (results.length) {
|
||||
// We only want 1 download, the most recent one
|
||||
results = NewTabUtils.activityStreamProvider._processHighlights(results, {numItems: 1}, "download");
|
||||
manyPages.push(...results);
|
||||
}
|
||||
}
|
||||
|
||||
const orderedPages = this._orderHighlights(manyPages);
|
||||
|
||||
// Remove adult highlights if we need to
|
||||
|
@ -157,8 +172,8 @@ this.HighlightsFeed = class HighlightsFeed {
|
|||
}
|
||||
|
||||
// If we already have the image for the card, use that immediately. Else
|
||||
// asynchronously fetch the image.
|
||||
if (!page.image) {
|
||||
// asynchronously fetch the image. NEVER fetch a screenshot for downloads
|
||||
if (!page.image && page.type !== "download") {
|
||||
this.fetchImage(page);
|
||||
}
|
||||
|
||||
|
@ -169,7 +184,7 @@ this.HighlightsFeed = class HighlightsFeed {
|
|||
|
||||
// We want the page, so update various fields for UI
|
||||
Object.assign(page, {
|
||||
hasImage: true, // We always have an image - fall back to a screenshot
|
||||
hasImage: page.type !== "download", // Downloads do not have an image - all else types fall back to a screenshot
|
||||
hostname,
|
||||
type: page.type,
|
||||
pocket_id: page.pocket_id
|
||||
|
@ -236,6 +251,8 @@ this.HighlightsFeed = class HighlightsFeed {
|
|||
}
|
||||
|
||||
onAction(action) {
|
||||
// Relay the downloads actions to DownloadsManager - it is a child of HighlightsFeed
|
||||
this.downloadsManager.onAction(action);
|
||||
switch (action.type) {
|
||||
case at.INIT:
|
||||
this.init();
|
||||
|
@ -247,6 +264,7 @@ this.HighlightsFeed = class HighlightsFeed {
|
|||
case at.MIGRATION_COMPLETED:
|
||||
case at.PLACES_HISTORY_CLEARED:
|
||||
case at.PLACES_LINK_BLOCKED:
|
||||
case at.DOWNLOAD_CHANGED:
|
||||
this.fetchHighlights({broadcast: true});
|
||||
break;
|
||||
case at.DELETE_FROM_POCKET:
|
||||
|
|
|
@ -13,6 +13,8 @@ ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|||
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "AppConstants",
|
||||
"resource://gre/modules/AppConstants.jsm");
|
||||
const ONBOARDING_FINISHED_PREF = "browser.onboarding.notification.finished";
|
||||
|
||||
// List of prefs that require migration to indexedDB.
|
||||
|
@ -97,8 +99,10 @@ this.PrefsFeed = class PrefsFeed {
|
|||
values[name] = this._prefs.get(name);
|
||||
}
|
||||
|
||||
// Not a pref, but we need this to determine whether to show private-browsing-related stuff
|
||||
// These are not prefs, but are needed to determine stuff in content that can only be
|
||||
// computed in main process
|
||||
values.isPrivateBrowsingEnabled = PrivateBrowsingUtils.enabled;
|
||||
values.platform = AppConstants.platform;
|
||||
|
||||
// Set the initial state of all prefs in redux
|
||||
this.store.dispatch(ac.BroadcastToContent({type: at.PREFS_INITIAL_VALUES, data: values}));
|
||||
|
|
|
@ -75,7 +75,8 @@ const SectionsManager = {
|
|||
CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES: {
|
||||
history: ["CheckBookmark", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
|
||||
bookmark: ["CheckBookmark", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
|
||||
pocket: ["ArchiveFromPocket", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"]
|
||||
pocket: ["ArchiveFromPocket", "CheckSavedToPocket", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
|
||||
download: ["OpenFile", "ShowFile", "Separator", "GoToDownloadPage", "CopyDownloadLink", "Separator", "RemoveDownload", "BlockUrl"]
|
||||
},
|
||||
initialized: false,
|
||||
sections: new Map(),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
|
||||
import {Card, PlaceholderCard} from "content-src/components/Card/Card";
|
||||
import {_Card as Card, PlaceholderCard} from "content-src/components/Card/Card";
|
||||
import {combineReducers, createStore} from "redux";
|
||||
import {INITIAL_STATE, reducers} from "common/Reducers.jsm";
|
||||
import {cardContextTypes} from "content-src/components/Card/types";
|
||||
|
@ -121,6 +121,32 @@ describe("<Card>", () => {
|
|||
button.simulate("click", {preventDefault: () => {}});
|
||||
assert.isTrue(wrapper.find(".card-outer").hasClass("active"));
|
||||
});
|
||||
it("should send SHOW_DOWNLOAD_FILE if we clicked on a download", () => {
|
||||
const downloadLink = {
|
||||
type: "download",
|
||||
url: "download.mov"
|
||||
};
|
||||
wrapper = mountCardWithProps(Object.assign({}, DEFAULT_PROPS, {link: downloadLink}));
|
||||
const card = wrapper.find(".card");
|
||||
card.simulate("click", {preventDefault: () => {}});
|
||||
assert.calledThrice(DEFAULT_PROPS.dispatch);
|
||||
|
||||
assert.equal(DEFAULT_PROPS.dispatch.firstCall.args[0].type, at.SHOW_DOWNLOAD_FILE);
|
||||
assert.deepEqual(DEFAULT_PROPS.dispatch.firstCall.args[0].data, downloadLink);
|
||||
});
|
||||
it("should send OPEN_LINK if we clicked on anything other than a download", () => {
|
||||
const nonDownloadLink = {
|
||||
type: "history",
|
||||
url: "download.mov"
|
||||
};
|
||||
wrapper = mountCardWithProps(Object.assign({}, DEFAULT_PROPS, {link: nonDownloadLink}));
|
||||
const card = wrapper.find(".card");
|
||||
const event = {altKey: "1", button: "2", ctrlKey: "3", metaKey: "4", shiftKey: "5"};
|
||||
card.simulate("click", Object.assign({}, event, {preventDefault: () => {}}));
|
||||
assert.calledThrice(DEFAULT_PROPS.dispatch);
|
||||
|
||||
assert.equal(DEFAULT_PROPS.dispatch.firstCall.args[0].type, at.OPEN_LINK);
|
||||
});
|
||||
describe("image loading", () => {
|
||||
let link;
|
||||
let triggerImage = {};
|
||||
|
@ -246,7 +272,7 @@ describe("<Card>", () => {
|
|||
|
||||
describe("<PlaceholderCard />", () => {
|
||||
it("should render a Card with placeholder=true", () => {
|
||||
const wrapper = shallow(<PlaceholderCard />);
|
||||
const wrapper = mountWithIntl(<Provider store={createStore(combineReducers(reducers), INITIAL_STATE)}><PlaceholderCard /></Provider>);
|
||||
assert.isTrue(wrapper.find(Card).props().placeholder);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -100,6 +100,53 @@ describe("<LinkMenu>", () => {
|
|||
const {options} = wrapper.find(ContextMenu).props();
|
||||
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_remove_bookmark")));
|
||||
});
|
||||
it("should show Open File option for a downloaded item", () => {
|
||||
wrapper = shallowWithIntl(<LinkMenu site={{url: "", type: "download", path: "foo"}} source={"HIGHLIGHTS"} options={["OpenFile"]} dispatch={() => {}} />);
|
||||
const {options} = wrapper.find(ContextMenu).props();
|
||||
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_open_file")));
|
||||
});
|
||||
it("should show Show File option for a downloaded item on a default platform", () => {
|
||||
wrapper = shallowWithIntl(<LinkMenu site={{url: "", type: "download", path: "foo"}} source={"HIGHLIGHTS"} options={["ShowFile"]} platform={"default"} dispatch={() => {}} />);
|
||||
const {options} = wrapper.find(ContextMenu).props();
|
||||
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_show_file_default")));
|
||||
});
|
||||
it("should show Show in Finder option for a downloaded item on a mac", () => {
|
||||
wrapper = shallowWithIntl(<LinkMenu site={{url: "", type: "download"}} source={"HIGHLIGHTS"} options={["ShowFile"]} platform={"macosx"} dispatch={() => {}} />);
|
||||
const {options} = wrapper.find(ContextMenu).props();
|
||||
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_show_file_mac_os")));
|
||||
});
|
||||
it("should show Open containing folder option for a downloaded item on windows", () => {
|
||||
wrapper = shallowWithIntl(<LinkMenu site={{url: "", type: "download"}} source={"HIGHLIGHTS"} options={["ShowFile"]} platform={"win"} dispatch={() => {}} />);
|
||||
const {options} = wrapper.find(ContextMenu).props();
|
||||
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_show_file_windows")));
|
||||
});
|
||||
it("should show Open containing folder option for a downloaded item on linux", () => {
|
||||
wrapper = shallowWithIntl(<LinkMenu site={{url: "", type: "download"}} source={"HIGHLIGHTS"} options={["ShowFile"]} platform={"linux"} dispatch={() => {}} />);
|
||||
const {options} = wrapper.find(ContextMenu).props();
|
||||
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_show_file_linux")));
|
||||
});
|
||||
it("should show Copy Downlad Link option for a downloaded item when CopyDownloadLink", () => {
|
||||
wrapper = shallowWithIntl(<LinkMenu site={{url: "", type: "download"}} source={"HIGHLIGHTS"} options={["CopyDownloadLink"]} dispatch={() => {}} />);
|
||||
const {options} = wrapper.find(ContextMenu).props();
|
||||
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_copy_download_link")));
|
||||
});
|
||||
it("should show Go To Download Page option for a downloaded item when GoToDownloadPage", () => {
|
||||
wrapper = shallowWithIntl(<LinkMenu site={{url: "", type: "download", referrer: "foo"}} source={"HIGHLIGHTS"} options={["GoToDownloadPage"]} dispatch={() => {}} />);
|
||||
const {options} = wrapper.find(ContextMenu).props();
|
||||
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_go_to_download_page")));
|
||||
assert.isFalse(options[0].disabled);
|
||||
});
|
||||
it("should show Go To Download Page option as disabled for a downloaded item when GoToDownloadPage if no referrer exists", () => {
|
||||
wrapper = shallowWithIntl(<LinkMenu site={{url: "", type: "download", referrer: null}} source={"HIGHLIGHTS"} options={["GoToDownloadPage"]} dispatch={() => {}} />);
|
||||
const {options} = wrapper.find(ContextMenu).props();
|
||||
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_go_to_download_page")));
|
||||
assert.isTrue(options[0].disabled);
|
||||
});
|
||||
it("should show Remove Download Link option for a downloaded item when RemoveDownload", () => {
|
||||
wrapper = shallowWithIntl(<LinkMenu site={{url: "", type: "download"}} source={"HIGHLIGHTS"} options={["RemoveDownload"]} dispatch={() => {}} />);
|
||||
const {options} = wrapper.find(ContextMenu).props();
|
||||
assert.isDefined(options.find(o => (o.id && o.id === "menu_action_remove_download")));
|
||||
});
|
||||
it("should show Edit option", () => {
|
||||
const props = {url: "foo", label: "label"};
|
||||
const index = 5;
|
||||
|
@ -129,9 +176,9 @@ describe("<LinkMenu>", () => {
|
|||
describe(".onClick", () => {
|
||||
const FAKE_INDEX = 3;
|
||||
const FAKE_SOURCE = "TOP_SITES";
|
||||
const FAKE_SITE = {url: "https://foo.com", pocket_id: "1234", referrer: "https://foo.com/ref", title: "bar", bookmarkGuid: 1234, hostname: "foo", type: "bookmark"};
|
||||
const FAKE_SITE = {url: "https://foo.com", pocket_id: "1234", referrer: "https://foo.com/ref", title: "bar", bookmarkGuid: 1234, hostname: "foo", type: "bookmark", path: "foo"};
|
||||
const dispatch = sinon.stub();
|
||||
const propOptions = ["Separator", "RemoveBookmark", "AddBookmark", "OpenInNewWindow", "OpenInPrivateWindow", "BlockUrl", "DeleteUrl", "PinTopSite", "UnpinTopSite", "SaveToPocket", "DeleteFromPocket", "ArchiveFromPocket", "WebExtDismiss"];
|
||||
const propOptions = ["ShowFile", "CopyDownloadLink", "GoToDownloadPage", "RemoveDownload", "Separator", "RemoveBookmark", "AddBookmark", "OpenInNewWindow", "OpenInPrivateWindow", "BlockUrl", "DeleteUrl", "PinTopSite", "UnpinTopSite", "SaveToPocket", "DeleteFromPocket", "ArchiveFromPocket", "WebExtDismiss"];
|
||||
const expectedActionData = {
|
||||
menu_action_remove_bookmark: FAKE_SITE.bookmarkGuid,
|
||||
menu_action_bookmark: {url: FAKE_SITE.url, title: FAKE_SITE.title, type: FAKE_SITE.type},
|
||||
|
@ -144,7 +191,11 @@ describe("<LinkMenu>", () => {
|
|||
menu_action_unpin: {site: {url: FAKE_SITE.url}},
|
||||
menu_action_save_to_pocket: {site: {url: FAKE_SITE.url, title: FAKE_SITE.title}},
|
||||
menu_action_delete_pocket: {pocket_id: "1234"},
|
||||
menu_action_archive_pocket: {pocket_id: "1234"}
|
||||
menu_action_archive_pocket: {pocket_id: "1234"},
|
||||
menu_action_show_file_default: {url: FAKE_SITE.url},
|
||||
menu_action_copy_download_link: {url: FAKE_SITE.url},
|
||||
menu_action_go_to_download_page: {url: FAKE_SITE.url},
|
||||
menu_action_remove_download: {url: FAKE_SITE.url}
|
||||
};
|
||||
|
||||
const {options} = shallowWithIntl(<LinkMenu
|
||||
|
@ -153,6 +204,7 @@ describe("<LinkMenu>", () => {
|
|||
dispatch={dispatch}
|
||||
index={FAKE_INDEX}
|
||||
isPrivateBrowsingEnabled={true}
|
||||
platform={"default"}
|
||||
options={propOptions}
|
||||
source={FAKE_SOURCE}
|
||||
shouldSendImpressionStats={true} />)
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import {combineReducers, createStore} from "redux";
|
||||
import {INITIAL_STATE, reducers} from "common/Reducers.jsm";
|
||||
import {mountWithIntl, shallowWithIntl} from "test/unit/utils";
|
||||
import {Section, SectionIntl, _Sections as Sections} from "content-src/components/Sections/Sections";
|
||||
import {actionTypes as at} from "common/Actions.jsm";
|
||||
import {PlaceholderCard} from "content-src/components/Card/Card";
|
||||
import {Provider} from "react-redux";
|
||||
import React from "react";
|
||||
import {SectionMenu} from "content-src/components/SectionMenu/SectionMenu";
|
||||
import {shallow} from "enzyme";
|
||||
import {TopSites} from "content-src/components/TopSites/TopSites";
|
||||
|
||||
function mountSectionWithProps(props) {
|
||||
const store = createStore(combineReducers(reducers), INITIAL_STATE);
|
||||
return mountWithIntl(<Provider store={store}><Section {...props} /></Provider>);
|
||||
}
|
||||
|
||||
describe("<Sections>", () => {
|
||||
let wrapper;
|
||||
let FAKE_SECTIONS;
|
||||
|
@ -67,24 +75,25 @@ describe("<Section>", () => {
|
|||
message: "Some message"
|
||||
}
|
||||
};
|
||||
|
||||
wrapper = shallowWithIntl(<Section {...FAKE_SECTION} />);
|
||||
wrapper = mountSectionWithProps(FAKE_SECTION);
|
||||
});
|
||||
|
||||
describe("context menu", () => {
|
||||
it("should render a context menu button", () => {
|
||||
wrapper = mountWithIntl(<Section {...FAKE_SECTION} />);
|
||||
wrapper = mountSectionWithProps(FAKE_SECTION);
|
||||
|
||||
assert.equal(wrapper.find(".section-top-bar .context-menu-button").length, 1);
|
||||
});
|
||||
it("should render a section menu when button is clicked", () => {
|
||||
wrapper = mountWithIntl(<Section {...FAKE_SECTION} />);
|
||||
wrapper = mountSectionWithProps(FAKE_SECTION);
|
||||
|
||||
const button = wrapper.find(".section-top-bar .context-menu-button");
|
||||
assert.equal(wrapper.find(SectionMenu).length, 0);
|
||||
button.simulate("click", {preventDefault: () => {}});
|
||||
assert.equal(wrapper.find(SectionMenu).length, 1);
|
||||
});
|
||||
it("should not render a section menu by default", () => {
|
||||
wrapper = mountWithIntl(<Section {...FAKE_SECTION} />);
|
||||
wrapper = shallowWithIntl(<Section {...FAKE_SECTION} />);
|
||||
assert.equal(wrapper.find(SectionMenu).length, 0);
|
||||
});
|
||||
});
|
||||
|
@ -93,8 +102,8 @@ describe("<Section>", () => {
|
|||
const CARDS_PER_ROW = 3;
|
||||
const fakeSite = {link: "http://localhost"};
|
||||
function renderWithSites(rows) {
|
||||
return shallowWithIntl(
|
||||
<Section {...FAKE_SECTION} rows={rows} maxRows={2} />);
|
||||
const store = createStore(combineReducers(reducers), INITIAL_STATE);
|
||||
return mountWithIntl(<Provider store={store}><Section {...FAKE_SECTION} rows={rows} maxRows={2} /></Provider>);
|
||||
}
|
||||
|
||||
it("should return 1 row of placeholders if realRows is 0", () => {
|
||||
|
@ -164,21 +173,21 @@ describe("<Section>", () => {
|
|||
};
|
||||
});
|
||||
it("should not render for empty topics", () => {
|
||||
wrapper = mountWithIntl(<Section {...TOP_STORIES_SECTION} />);
|
||||
wrapper = mountSectionWithProps(TOP_STORIES_SECTION);
|
||||
|
||||
assert.lengthOf(wrapper.find(".topic"), 0);
|
||||
});
|
||||
it("should render for non-empty topics", () => {
|
||||
TOP_STORIES_SECTION.topics = [{name: "topic1", url: "topic-url1"}];
|
||||
|
||||
wrapper = mountWithIntl(<Section {...TOP_STORIES_SECTION} />);
|
||||
wrapper = mountSectionWithProps(TOP_STORIES_SECTION);
|
||||
|
||||
assert.lengthOf(wrapper.find(".topic"), 1);
|
||||
});
|
||||
it("should render for uninitialized topics", () => {
|
||||
delete TOP_STORIES_SECTION.topics;
|
||||
|
||||
wrapper = mountWithIntl(<Section {...TOP_STORIES_SECTION} />);
|
||||
wrapper = mountSectionWithProps(TOP_STORIES_SECTION);
|
||||
|
||||
assert.lengthOf(wrapper.find(".topic"), 1);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
import {actionTypes as at} from "common/Actions.jsm";
|
||||
import {DownloadsManager} from "lib/DownloadsManager.jsm";
|
||||
import {GlobalOverrider} from "test/unit/utils";
|
||||
|
||||
describe("Downloads Manager", () => {
|
||||
let downloadsManager;
|
||||
let globals;
|
||||
let download;
|
||||
let sandbox;
|
||||
const DOWNLOAD_URL = "https://site.com/download.mov";
|
||||
|
||||
beforeEach(() => {
|
||||
globals = new GlobalOverrider();
|
||||
sandbox = globals.sandbox;
|
||||
global.Cc["@mozilla.org/widget/clipboardhelper;1"] = {
|
||||
getService() {
|
||||
return {copyString: sinon.stub()};
|
||||
}
|
||||
};
|
||||
global.Cc["@mozilla.org/timer;1"] = {
|
||||
createInstance() {
|
||||
return {
|
||||
initWithCallback: sinon.stub().callsFake(callback => callback()),
|
||||
cancel: sinon.spy()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
globals.set("DownloadsCommon", {
|
||||
getData: sinon.stub().returns({
|
||||
addView: sinon.stub(),
|
||||
removeView: sinon.stub()
|
||||
})
|
||||
});
|
||||
sandbox.stub(global.DownloadsViewUI.DownloadElementShell.prototype, "downloadsCmd_open");
|
||||
sandbox.stub(global.DownloadsViewUI.DownloadElementShell.prototype, "downloadsCmd_show");
|
||||
sandbox.stub(global.DownloadsViewUI.DownloadElementShell.prototype, "downloadsCmd_openReferrer");
|
||||
sandbox.stub(global.DownloadsViewUI.DownloadElementShell.prototype, "downloadsCmd_delete");
|
||||
|
||||
downloadsManager = new DownloadsManager();
|
||||
downloadsManager.init({dispatch() {}});
|
||||
downloadsManager.onDownloadAdded({
|
||||
source: {url: DOWNLOAD_URL},
|
||||
endTime: Date.now(),
|
||||
target: {path: "/path/to/download.mov", exists: true},
|
||||
succeeded: true,
|
||||
refresh: async () => {}
|
||||
});
|
||||
download = downloadsManager._viewableDownloadItems.get(DOWNLOAD_URL);
|
||||
});
|
||||
afterEach(() => {
|
||||
downloadsManager._viewableDownloadItems.clear();
|
||||
globals.restore();
|
||||
});
|
||||
describe("#init", () => {
|
||||
it("should add a DownloadsCommon view on init", () => {
|
||||
downloadsManager.init({dispatch() {}});
|
||||
assert.calledTwice(global.DownloadsCommon.getData().addView);
|
||||
});
|
||||
});
|
||||
describe("#onAction", () => {
|
||||
it("should copy the file on COPY_DOWNLOAD_LINK", () => {
|
||||
sinon.spy(download, "downloadsCmd_copyLocation");
|
||||
downloadsManager.onAction({type: at.COPY_DOWNLOAD_LINK, data: {url: DOWNLOAD_URL}});
|
||||
assert.calledOnce(download.downloadsCmd_copyLocation);
|
||||
});
|
||||
it("should go to the file on GO_TO_DOWNLOAD_PAGE", () => {
|
||||
sinon.spy(download, "downloadsCmd_openReferrer");
|
||||
downloadsManager.onAction({type: at.GO_TO_DOWNLOAD_PAGE, data: {url: DOWNLOAD_URL}});
|
||||
assert.calledOnce(download.downloadsCmd_openReferrer);
|
||||
});
|
||||
it("should remove the file on REMOVE_DOWNLOAD_FILE", () => {
|
||||
downloadsManager.onAction({type: at.REMOVE_DOWNLOAD_FILE, data: {url: DOWNLOAD_URL}});
|
||||
assert.calledOnce(download.downloadsCmd_delete);
|
||||
});
|
||||
it("should show the file on SHOW_DOWNLOAD_FILE", () => {
|
||||
downloadsManager.onAction({type: at.SHOW_DOWNLOAD_FILE, data: {url: DOWNLOAD_URL}});
|
||||
assert.calledOnce(download.downloadsCmd_show);
|
||||
});
|
||||
it("should open the file on OPEN_DOWNLOAD_FILE if the type is download", () => {
|
||||
downloadsManager.onAction({type: at.OPEN_DOWNLOAD_FILE, data: {url: DOWNLOAD_URL, type: "download"}});
|
||||
assert.calledOnce(download.downloadsCmd_open);
|
||||
});
|
||||
it("should copy the file on UNINIT", () => {
|
||||
// DownloadsManager._downloadData needs to exist first
|
||||
downloadsManager.onAction({type: at.UNINIT});
|
||||
assert.calledOnce(global.DownloadsCommon.getData().removeView);
|
||||
});
|
||||
it("should not execute a download command if we do not have the correct url", () => {
|
||||
downloadsManager.onAction({type: at.SHOW_DOWNLOAD_FILE, data: {url: "unknown_url"}});
|
||||
assert.notCalled(download.downloadsCmd_show);
|
||||
});
|
||||
});
|
||||
describe("#onDownloadAdded", () => {
|
||||
let newDownload;
|
||||
beforeEach(() => {
|
||||
downloadsManager._viewableDownloadItems.clear();
|
||||
newDownload = {
|
||||
source: {url: "https://site.com/newDownload.mov"},
|
||||
endTime: Date.now(),
|
||||
target: {path: "/path/to/newDownload.mov", exists: true},
|
||||
succeeded: true,
|
||||
refresh: async () => {}
|
||||
};
|
||||
});
|
||||
afterEach(() => {
|
||||
downloadsManager._viewableDownloadItems.clear();
|
||||
});
|
||||
it("should add a download on onDownloadAdded", () => {
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
const result = downloadsManager._viewableDownloadItems.get("https://site.com/newDownload.mov");
|
||||
assert.ok(result);
|
||||
assert.instanceOf(result, global.DownloadsViewUI.DownloadElementShell);
|
||||
});
|
||||
it("should not add a download if it already exists", () => {
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
const results = downloadsManager._viewableDownloadItems;
|
||||
assert.equal(results.size, 1);
|
||||
});
|
||||
it("should not return any downloads if no threshold is provided", async () => {
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
const results = await downloadsManager.getDownloads(null, {});
|
||||
assert.equal(results.length, 0);
|
||||
});
|
||||
it("should stop at numItems when it found one it's looking for", async () => {
|
||||
const aDownload = {
|
||||
source: {url: "https://site.com/aDownload.pdf"},
|
||||
endTime: Date.now(),
|
||||
target: {path: "/path/to/aDownload.pdf", exists: true},
|
||||
succeeded: true,
|
||||
refresh: async () => {}
|
||||
};
|
||||
downloadsManager.onDownloadAdded(aDownload);
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
const results = await downloadsManager.getDownloads(Infinity, {numItems: 1, onlySucceeded: true, onlyExists: true});
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].url, aDownload.source.url);
|
||||
});
|
||||
it("should get all the downloads younger than the threshold provided", async () => {
|
||||
const oldDownload = {
|
||||
source: {url: "https://site.com/oldDownload.pdf"},
|
||||
endTime: Date.now() - 40 * 60 * 60 * 1000,
|
||||
target: {path: "/path/to/oldDownload.pdf", exists: true},
|
||||
succeeded: true,
|
||||
refresh: async () => {}
|
||||
};
|
||||
// Add an old download (older than 36 hours in this case)
|
||||
downloadsManager.onDownloadAdded(oldDownload);
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
const RECENT_DOWNLOAD_THRESHOLD = 36 * 60 * 60 * 1000;
|
||||
const results = await downloadsManager.getDownloads(RECENT_DOWNLOAD_THRESHOLD, {numItems: 5, onlySucceeded: true, onlyExists: true});
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].url, newDownload.source.url);
|
||||
});
|
||||
it("should dispatch DOWNLOAD_CHANGED when adding a download", () => {
|
||||
downloadsManager._store.dispatch = sinon.spy();
|
||||
downloadsManager._downloadTimer = null; // Nuke the timer
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
assert.calledOnce(downloadsManager._store.dispatch);
|
||||
});
|
||||
it("should refresh the downloads if onlyExists is true", async () => {
|
||||
const aDownload = {
|
||||
source: {url: "https://site.com/aDownload.pdf"},
|
||||
endTime: Date.now() - 40 * 60 * 60 * 1000,
|
||||
target: {path: "/path/to/aDownload.pdf", exists: true},
|
||||
succeeded: true,
|
||||
refresh: () => {}
|
||||
};
|
||||
sinon.stub(aDownload, "refresh").returns(Promise.resolve());
|
||||
downloadsManager.onDownloadAdded(aDownload);
|
||||
await downloadsManager.getDownloads(Infinity, {numItems: 5, onlySucceeded: true, onlyExists: true});
|
||||
assert.calledOnce(aDownload.refresh);
|
||||
});
|
||||
it("should not refresh the downloads if onlyExists is false (by default)", async () => {
|
||||
const aDownload = {
|
||||
source: {url: "https://site.com/aDownload.pdf"},
|
||||
endTime: Date.now() - 40 * 60 * 60 * 1000,
|
||||
target: {path: "/path/to/aDownload.pdf", exists: true},
|
||||
succeeded: true,
|
||||
refresh: () => {}
|
||||
};
|
||||
sinon.stub(aDownload, "refresh").returns(Promise.resolve());
|
||||
downloadsManager.onDownloadAdded(aDownload);
|
||||
await downloadsManager.getDownloads(Infinity, {numItems: 5, onlySucceeded: true});
|
||||
assert.notCalled(aDownload.refresh);
|
||||
});
|
||||
it("should only return downloads that exist if specified", async () => {
|
||||
const nonExistantDownload = {
|
||||
source: {url: "https://site.com/nonExistantDownload.pdf"},
|
||||
endTime: Date.now() - 40 * 60 * 60 * 1000,
|
||||
target: {path: "/path/to/nonExistantDownload.pdf", exists: false},
|
||||
succeeded: true,
|
||||
refresh: async () => {}
|
||||
};
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
downloadsManager.onDownloadAdded(nonExistantDownload);
|
||||
const results = await downloadsManager.getDownloads(Infinity, {numItems: 5, onlySucceeded: true, onlyExists: true});
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].url, newDownload.source.url);
|
||||
});
|
||||
it("should return all downloads that either exist or don't exist if not specified", async () => {
|
||||
const nonExistantDownload = {
|
||||
source: {url: "https://site.com/nonExistantDownload.pdf"},
|
||||
endTime: Date.now() - 40 * 60 * 60 * 1000,
|
||||
target: {path: "/path/to/nonExistantDownload.pdf", exists: false},
|
||||
succeeded: true,
|
||||
refresh: async () => {}
|
||||
};
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
downloadsManager.onDownloadAdded(nonExistantDownload);
|
||||
const results = await downloadsManager.getDownloads(Infinity, {numItems: 5, onlySucceeded: true});
|
||||
assert.equal(results.length, 2);
|
||||
assert.equal(results[0].url, newDownload.source.url);
|
||||
assert.equal(results[1].url, nonExistantDownload.source.url);
|
||||
});
|
||||
it("should only return downloads that were successful if specified", async () => {
|
||||
const nonSuccessfulDownload = {
|
||||
source: {url: "https://site.com/nonSuccessfulDownload.pdf"},
|
||||
endTime: Date.now() - 40 * 60 * 60 * 1000,
|
||||
target: {path: "/path/to/nonSuccessfulDownload.pdf", exists: false},
|
||||
succeeded: false,
|
||||
refresh: async () => {}
|
||||
};
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
downloadsManager.onDownloadAdded(nonSuccessfulDownload);
|
||||
const results = await downloadsManager.getDownloads(Infinity, {numItems: 5, onlySucceeded: true});
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].url, newDownload.source.url);
|
||||
});
|
||||
it("should return all downloads that were either successful or not if not specified", async () => {
|
||||
const nonExistantDownload = {
|
||||
source: {url: "https://site.com/nonExistantDownload.pdf"},
|
||||
endTime: Date.now() - 40 * 60 * 60 * 1000,
|
||||
target: {path: "/path/to/nonExistantDownload.pdf", exists: true},
|
||||
succeeded: false,
|
||||
refresh: async () => {}
|
||||
};
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
downloadsManager.onDownloadAdded(nonExistantDownload);
|
||||
const results = await downloadsManager.getDownloads(Infinity, {numItems: 5});
|
||||
assert.equal(results.length, 2);
|
||||
assert.equal(results[0].url, newDownload.source.url);
|
||||
assert.equal(results[1].url, nonExistantDownload.source.url);
|
||||
});
|
||||
it("should sort the downloads by recency", async () => {
|
||||
const olderDownload1 = {
|
||||
source: {url: "https://site.com/oldDownload1.pdf"},
|
||||
endTime: Date.now() - 2 * 60 * 60 * 1000, // 2 hours ago
|
||||
target: {path: "/path/to/oldDownload1.pdf", exists: true},
|
||||
succeeded: true,
|
||||
refresh: async () => {}
|
||||
};
|
||||
const olderDownload2 = {
|
||||
source: {url: "https://site.com/oldDownload2.pdf"},
|
||||
endTime: Date.now() - 60 * 60 * 1000, // 1 hour ago
|
||||
target: {path: "/path/to/oldDownload2.pdf", exists: true},
|
||||
succeeded: true,
|
||||
refresh: async () => {}
|
||||
};
|
||||
// Add some older downloads and check that they are in order
|
||||
downloadsManager.onDownloadAdded(olderDownload1);
|
||||
downloadsManager.onDownloadAdded(olderDownload2);
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
const results = await downloadsManager.getDownloads(Infinity, {numItems: 5, onlySucceeded: true, onlyExists: true});
|
||||
assert.equal(results.length, 3);
|
||||
assert.equal(results[0].url, newDownload.source.url);
|
||||
assert.equal(results[1].url, olderDownload2.source.url);
|
||||
assert.equal(results[2].url, olderDownload1.source.url);
|
||||
});
|
||||
it("should format the description properly if there is no file type", async () => {
|
||||
newDownload.target.path = null;
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
const results = await downloadsManager.getDownloads(Infinity, {numItems: 5, onlySucceeded: true, onlyExists: true});
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].description, "1.5 MB"); // see unit-entry.js to see where this comes from
|
||||
});
|
||||
});
|
||||
describe("#onDownloadRemoved", () => {
|
||||
let newDownload;
|
||||
beforeEach(() => {
|
||||
downloadsManager._viewableDownloadItems.clear();
|
||||
newDownload = {
|
||||
source: {url: "https://site.com/removeMe.mov"},
|
||||
endTime: Date.now(),
|
||||
target: {path: "/path/to/removeMe.mov", exists: true},
|
||||
succeeded: true,
|
||||
refresh: async () => {}
|
||||
};
|
||||
downloadsManager.onDownloadAdded(newDownload);
|
||||
});
|
||||
it("should remove a download if it exists on onDownloadRemoved", async () => {
|
||||
downloadsManager.onDownloadRemoved({source: {url: "https://site.com/removeMe.mov"}});
|
||||
const results = await downloadsManager.getDownloads(Infinity, {numItems: 5});
|
||||
assert.deepEqual(results, []);
|
||||
});
|
||||
it("should dispatch DOWNLOAD_CHANGED when removing a download", () => {
|
||||
downloadsManager._store.dispatch = sinon.spy();
|
||||
downloadsManager.onDownloadRemoved({source: {url: "https://site.com/removeMe.mov"}});
|
||||
assert.calledOnce(downloadsManager._store.dispatch);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -24,6 +24,7 @@ describe("Highlights Feed", () => {
|
|||
let fakeNewTabUtils;
|
||||
let filterAdultStub;
|
||||
let sectionsManagerStub;
|
||||
let downloadsManagerStub;
|
||||
let shortURLStub;
|
||||
let fakePageThumbs;
|
||||
|
||||
|
@ -35,7 +36,8 @@ describe("Highlights Feed", () => {
|
|||
getHighlights: sandbox.spy(() => Promise.resolve(links)),
|
||||
deletePocketEntry: sandbox.spy(() => Promise.resolve({})),
|
||||
archivePocketEntry: sandbox.spy(() => Promise.resolve({}))
|
||||
}
|
||||
},
|
||||
activityStreamProvider: {_processHighlights: sandbox.spy(l => l.slice(0, 1))}
|
||||
};
|
||||
sectionsManagerStub = {
|
||||
onceInitialized: sinon.stub().callsFake(callback => callback()),
|
||||
|
@ -45,6 +47,11 @@ describe("Highlights Feed", () => {
|
|||
updateSectionCard: sinon.spy(),
|
||||
sections: new Map([["highlights", {id: "highlights"}]])
|
||||
};
|
||||
downloadsManagerStub = sinon.stub().returns({
|
||||
getDownloads: () => [{"url": "https://site.com/download"}],
|
||||
onAction: sinon.spy(),
|
||||
init: sinon.spy()
|
||||
});
|
||||
fakeScreenshot = {
|
||||
getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_IMAGE)),
|
||||
maybeCacheScreenshot: Screenshots.maybeCacheScreenshot,
|
||||
|
@ -64,7 +71,8 @@ describe("Highlights Feed", () => {
|
|||
"lib/ShortURL.jsm": {shortURL: shortURLStub},
|
||||
"lib/SectionsManager.jsm": {SectionsManager: sectionsManagerStub},
|
||||
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot},
|
||||
"common/Dedupe.jsm": {Dedupe}
|
||||
"common/Dedupe.jsm": {Dedupe},
|
||||
"lib/DownloadsManager.jsm": {DownloadsManager: downloadsManagerStub}
|
||||
}));
|
||||
sandbox.spy(global.Services.obs, "addObserver");
|
||||
sandbox.spy(global.Services.obs, "removeObserver");
|
||||
|
@ -73,7 +81,7 @@ describe("Highlights Feed", () => {
|
|||
dispatch: sinon.spy(),
|
||||
getState() { return this.state; },
|
||||
state: {
|
||||
Prefs: {values: {"filterAdult": false, "section.highlights.includePocket": false}},
|
||||
Prefs: {values: {"filterAdult": false, "section.highlights.includePocket": false, "section.highlights.includeDownloads": false}},
|
||||
TopSites: {
|
||||
initialized: true,
|
||||
rows: Array(12).fill(null).map((v, i) => ({url: `http://www.topsite${i}.com`}))
|
||||
|
@ -115,6 +123,10 @@ describe("Highlights Feed", () => {
|
|||
feed.postInit();
|
||||
assert.calledOnce(feed.fetchHighlights);
|
||||
});
|
||||
it("should hook up the store for the DownloadsManager", () => {
|
||||
feed.onAction({type: at.INIT});
|
||||
assert.calledOnce(feed.downloadsManager.init);
|
||||
});
|
||||
});
|
||||
describe("#observe", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -315,17 +327,19 @@ describe("Highlights Feed", () => {
|
|||
assert.equal(highlights[0].url, links[0].url);
|
||||
assert.equal(highlights[1].url, links[2].url);
|
||||
});
|
||||
it("should take both a bookmark and a pocket of the same hostname", async () => {
|
||||
it("should take a bookmark, a pocket, and downloaded item of the same hostname", async () => {
|
||||
links = [
|
||||
{url: "https://site.com/bookmark", type: "bookmark"},
|
||||
{url: "https://site.com/pocket", type: "pocket"}
|
||||
{url: "https://site.com/pocket", type: "pocket"},
|
||||
{url: "https://site.com/download", type: "download"}
|
||||
];
|
||||
|
||||
const highlights = await fetchHighlights();
|
||||
|
||||
assert.equal(highlights.length, 2);
|
||||
assert.equal(highlights.length, 3);
|
||||
assert.equal(highlights[0].url, links[0].url);
|
||||
assert.equal(highlights[1].url, links[1].url);
|
||||
assert.equal(highlights[2].url, links[2].url);
|
||||
});
|
||||
it("should includePocket pocket items when pref is true", async () => {
|
||||
feed.store.state.Prefs.values["section.highlights.includePocket"] = true;
|
||||
|
@ -340,6 +354,52 @@ describe("Highlights Feed", () => {
|
|||
|
||||
assert.calledWith(feed.linksCache.request, {numItems: MANY_EXTRA_LENGTH, excludePocket: true});
|
||||
});
|
||||
it("should not include downloads when includeDownloads pref is false", async () => {
|
||||
links = [
|
||||
{url: "https://site.com/bookmark", type: "bookmark"},
|
||||
{url: "https://site.com/pocket", type: "pocket"}
|
||||
];
|
||||
|
||||
// Check that we don't have the downloaded item in highlights
|
||||
const highlights = await fetchHighlights();
|
||||
assert.equal(highlights.length, 2);
|
||||
assert.equal(highlights[0].url, links[0].url);
|
||||
assert.equal(highlights[1].url, links[1].url);
|
||||
|
||||
assert.notCalled(global.NewTabUtils.activityStreamProvider._processHighlights);
|
||||
});
|
||||
it("should include downloads when includeDownloads pref is true", async () => {
|
||||
feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true;
|
||||
links = [
|
||||
{url: "https://site.com/bookmark", type: "bookmark"},
|
||||
{url: "https://site.com/pocket", type: "pocket"}
|
||||
];
|
||||
|
||||
// Check that we did get the downloaded item in highlights
|
||||
const highlights = await fetchHighlights();
|
||||
assert.equal(highlights.length, 3);
|
||||
assert.equal(highlights[0].url, links[0].url);
|
||||
assert.equal(highlights[1].url, links[1].url);
|
||||
assert.equal(highlights[2].url, "https://site.com/download");
|
||||
|
||||
assert.calledOnce(global.NewTabUtils.activityStreamProvider._processHighlights);
|
||||
});
|
||||
it("should only take 1 download", async () => {
|
||||
feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true;
|
||||
feed.downloadsManager.getDownloads = () => [
|
||||
{"url": "https://site1.com/download"},
|
||||
{"url": "https://site2.com/download"}
|
||||
];
|
||||
links = [{url: "https://site.com/bookmark", type: "bookmark"}];
|
||||
|
||||
// Check that we did get the most single recent downloaded item in highlights
|
||||
const highlights = await fetchHighlights();
|
||||
assert.equal(highlights.length, 2);
|
||||
assert.equal(highlights[0].url, links[0].url);
|
||||
assert.equal(highlights[1].url, "https://site1.com/download");
|
||||
|
||||
assert.calledOnce(global.NewTabUtils.activityStreamProvider._processHighlights);
|
||||
});
|
||||
it("should set type to bookmark if there is a bookmarkGuid", async () => {
|
||||
links = [{url: "https://mozilla.org", type: "history", bookmarkGuid: "1234567890"}];
|
||||
|
||||
|
@ -451,6 +511,11 @@ describe("Highlights Feed", () => {
|
|||
});
|
||||
});
|
||||
describe("#onAction", () => {
|
||||
it("should relay all actions to DownloadsManager.onAction", () => {
|
||||
let action = {type: at.COPY_DOWNLOAD_LINK, data: {url: "foo.png"}, _target: {}};
|
||||
feed.onAction(action);
|
||||
assert.calledWith(feed.downloadsManager.onAction, action);
|
||||
});
|
||||
it("should fetch highlights on SYSTEM_TICK", async () => {
|
||||
await feed.fetchHighlights();
|
||||
feed.fetchHighlights = sinon.spy();
|
||||
|
@ -473,6 +538,13 @@ describe("Highlights Feed", () => {
|
|||
assert.calledOnce(feed.fetchHighlights);
|
||||
assert.calledWith(feed.fetchHighlights, {broadcast: true});
|
||||
});
|
||||
it("should fetch highlights on DOWNLOAD_CHANGED", async () => {
|
||||
await feed.fetchHighlights();
|
||||
feed.fetchHighlights = sinon.spy();
|
||||
feed.onAction({type: at.DOWNLOAD_CHANGED});
|
||||
assert.calledOnce(feed.fetchHighlights);
|
||||
assert.calledWith(feed.fetchHighlights, {broadcast: true});
|
||||
});
|
||||
it("should fetch highlights on PLACES_LINKS_CHANGED", async () => {
|
||||
await feed.fetchHighlights();
|
||||
feed.fetchHighlights = sinon.spy();
|
||||
|
|
|
@ -4,6 +4,15 @@ import {chaiAssertions} from "test/schemas/pings";
|
|||
import enzyme from "enzyme";
|
||||
enzyme.configure({adapter: new Adapter()});
|
||||
|
||||
class DownloadElementShell {
|
||||
downloadsCmd_open() {}
|
||||
downloadsCmd_show() {}
|
||||
downloadsCmd_openReferrer() {}
|
||||
downloadsCmd_delete() {}
|
||||
get sizeStrings() { return {stateLabel: "1.5 MB"}; }
|
||||
displayName() {}
|
||||
}
|
||||
|
||||
// Cause React warnings to make tests that trigger them fail
|
||||
const origConsoleError = console.error; // eslint-disable-line no-console
|
||||
console.error = function(msg, ...args) { // eslint-disable-line no-console
|
||||
|
@ -84,6 +93,7 @@ const TEST_GLOBAL = {
|
|||
},
|
||||
PluralForm: {get() {}},
|
||||
Preferences: FakePrefs,
|
||||
DownloadsViewUI: {DownloadElementShell},
|
||||
Services: {
|
||||
locale: {
|
||||
getAppLocaleAsLangTag() { return "en-US"; },
|
||||
|
@ -95,7 +105,12 @@ const TEST_GLOBAL = {
|
|||
addMessageListener: (msg, cb) => cb(),
|
||||
removeMessageListener() {}
|
||||
},
|
||||
appShell: {hiddenDOMWindow: {performance: new FakePerformance()}},
|
||||
appShell: {
|
||||
hiddenDOMWindow: {
|
||||
openNewTabWith() {},
|
||||
performance: new FakePerformance()
|
||||
}
|
||||
},
|
||||
obs: {
|
||||
addObserver() {},
|
||||
removeObserver() {}
|
||||
|
|
Загрузка…
Ссылка в новой задаче