feat(highlights): Lazily add DownloadsManager view to handle recent downloads (#4076)

Fix Bug 1433230 - Add recent downloads to Highlights
This commit is contained in:
Ursula Sarracini 2018-04-19 23:05:20 -04:00 коммит произвёл Ed Lee
Родитель 86050a1c4d
Коммит e87a190e15
22 изменённых файлов: 876 добавлений и 41 удалений

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

@ -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() {}