import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,177 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Handles the download progress indicator in the taskbar.
*/
"use strict";
this.EXPORTED_SYMBOLS = [
"DownloadsTaskbar",
];
////////////////////////////////////////////////////////////////////////////////
//// Globals
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGetter(this, "gWinTaskbar", function () {
if (!("@mozilla.org/windows-taskbar;1" in Cc)) {
return null;
}
let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"]
.getService(Ci.nsIWinTaskbar);
return winTaskbar.available && winTaskbar;
});
XPCOMUtils.defineLazyGetter(this, "gMacTaskbarProgress", function () {
return ("@mozilla.org/widget/macdocksupport;1" in Cc) &&
Cc["@mozilla.org/widget/macdocksupport;1"]
.getService(Ci.nsITaskbarProgress);
});
////////////////////////////////////////////////////////////////////////////////
//// DownloadsTaskbar
/**
* Handles the download progress indicator in the taskbar.
*/
this.DownloadsTaskbar = {
/**
* Underlying DownloadSummary providing the aggregate download information, or
* null if the indicator has never been initialized.
*/
_summary: null,
/**
* nsITaskbarProgress object to which download information is dispatched.
* This can be null if the indicator has never been initialized or if the
* indicator is currently hidden on Windows.
*/
_taskbarProgress: null,
/**
* This method is called after a new browser window is opened, and ensures
* that the download progress indicator is displayed in the taskbar.
*
* On Windows, the indicator is attached to the first browser window that
* calls this method. When the window is closed, the indicator is moved to
* another browser window, if available, in no particular order. When there
* are no browser windows visible, the indicator is hidden.
*
* On Mac OS X, the indicator is initialized globally when this method is
* called for the first time. Subsequent calls have no effect.
*
* @param aBrowserWindow
* nsIDOMWindow object of the newly opened browser window to which the
* indicator may be attached.
*/
registerIndicator(aBrowserWindow) {
if (!this._taskbarProgress) {
if (gMacTaskbarProgress) {
// On Mac OS X, we have to register the global indicator only once.
this._taskbarProgress = gMacTaskbarProgress;
// Free the XPCOM reference on shutdown, to prevent detecting a leak.
Services.obs.addObserver(() => {
this._taskbarProgress = null;
gMacTaskbarProgress = null;
}, "quit-application-granted", false);
} else if (gWinTaskbar) {
// On Windows, the indicator is currently hidden because we have no
// previous browser window, thus we should attach the indicator now.
this._attachIndicator(aBrowserWindow);
} else {
// The taskbar indicator is not available on this platform.
return;
}
}
// Ensure that the DownloadSummary object will be created asynchronously.
if (!this._summary) {
Downloads.getSummary(Downloads.ALL).then(summary => {
// In case the method is re-entered, we simply ignore redundant
// invocations of the callback, instead of keeping separate state.
if (this._summary) {
return;
}
this._summary = summary;
return this._summary.addView(this);
}).then(null, Cu.reportError);
}
},
/**
* On Windows, attaches the taskbar indicator to the specified browser window.
*/
_attachIndicator(aWindow) {
// Activate the indicator on the specified window.
let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIXULWindow).docShell;
this._taskbarProgress = gWinTaskbar.getTaskbarProgress(docShell);
// If the DownloadSummary object has already been created, we should update
// the state of the new indicator, otherwise it will be updated as soon as
// the DownloadSummary view is registered.
if (this._summary) {
this.onSummaryChanged();
}
aWindow.addEventListener("unload", () => {
// Locate another browser window, excluding the one being closed.
let browserWindow = RecentWindow.getMostRecentBrowserWindow();
if (browserWindow) {
// Move the progress indicator to the other browser window.
this._attachIndicator(browserWindow);
} else {
// The last browser window has been closed. We remove the reference to
// the taskbar progress object so that the indicator will be registered
// again on the next browser window that is opened.
this._taskbarProgress = null;
}
}, false);
},
//////////////////////////////////////////////////////////////////////////////
//// DownloadSummary view
onSummaryChanged() {
// If the last browser window has been closed, we have no indicator any more.
if (!this._taskbarProgress) {
return;
}
if (this._summary.allHaveStopped || this._summary.progressTotalBytes == 0) {
this._taskbarProgress.setProgressState(
Ci.nsITaskbarProgress.STATE_NO_PROGRESS, 0, 0);
} else {
// For a brief moment before completion, some download components may
// report more transferred bytes than the total number of bytes. Thus,
// ensure that we never break the expectations of the progress indicator.
let progressCurrentBytes = Math.min(this._summary.progressTotalBytes,
this._summary.progressCurrentBytes);
this._taskbarProgress.setProgressState(
Ci.nsITaskbarProgress.STATE_NORMAL,
progressCurrentBytes,
this._summary.progressTotalBytes);
}
},
};

View file

@ -0,0 +1,395 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* This module is imported by code that uses the "download.xml" binding, and
* provides prototypes for objects that handle input and display information.
*/
"use strict";
this.EXPORTED_SYMBOLS = [
"DownloadsViewUI",
];
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
"resource://gre/modules/DownloadUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
"resource:///modules/DownloadsCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
this.DownloadsViewUI = {
/**
* Returns true if the given string is the name of a command that can be
* handled by the Downloads user interface, including standard commands.
*/
isCommandName(name) {
return name.startsWith("cmd_") || name.startsWith("downloadsCmd_");
},
};
/**
* A download element shell is responsible for handling the commands and the
* displayed data for a single element that uses the "download.xml" binding.
*
* The information to display is obtained through the associated Download object
* from the JavaScript API for downloads, and commands are executed using a
* combination of Download methods and DownloadsCommon.jsm helper functions.
*
* Specialized versions of this shell must be defined, and they are required to
* implement the "download" property or getter. Currently these objects are the
* HistoryDownloadElementShell and the DownloadsViewItem for the panel. The
* history view may use a HistoryDownload object in place of a Download object.
*/
this.DownloadsViewUI.DownloadElementShell = function () {}
this.DownloadsViewUI.DownloadElementShell.prototype = {
/**
* The richlistitem for the download, initialized by the derived object.
*/
element: null,
/**
* URI string for the file type icon displayed in the download element.
*/
get image() {
if (!this.download.target.path) {
// Old history downloads may not have a target path.
return "moz-icon://.unknown?size=32";
}
// When a download that was previously in progress finishes successfully, it
// means that the target file now exists and we can extract its specific
// icon, for example from a Windows executable. To ensure that the icon is
// reloaded, however, we must change the URI used by the XUL image element,
// for example by adding a query parameter. This only works if we add one of
// the parameters explicitly supported by the nsIMozIconURI interface.
return "moz-icon://" + this.download.target.path + "?size=32" +
(this.download.succeeded ? "&state=normal" : "");
},
/**
* The user-facing label for the download. This is normally the leaf name of
* the download target file. In case this is a very old history download for
* which the target file is unknown, the download source URI is displayed.
*/
get displayName() {
if (!this.download.target.path) {
return this.download.source.url;
}
return OS.Path.basename(this.download.target.path);
},
/**
* The progress element for the download, or undefined in case the XBL binding
* has not been applied yet.
*/
get _progressElement() {
if (!this.__progressElement) {
// If the element is not available now, we will try again the next time.
this.__progressElement =
this.element.ownerDocument.getAnonymousElementByAttribute(
this.element, "anonid",
"progressmeter");
}
return this.__progressElement;
},
/**
* Processes a major state change in the user interface, then proceeds with
* the normal progress update. This function is not called for every progress
* update in order to improve performance.
*/
_updateState() {
this.element.setAttribute("displayName", this.displayName);
this.element.setAttribute("image", this.image);
this.element.setAttribute("state",
DownloadsCommon.stateOfDownload(this.download));
if (!this.download.succeeded && this.download.error &&
this.download.error.becauseBlockedByReputationCheck) {
this.element.setAttribute("verdict",
this.download.error.reputationCheckVerdict);
} else {
this.element.removeAttribute("verdict");
}
// Since state changed, reset the time left estimation.
this.lastEstimatedSecondsLeft = Infinity;
this._updateProgress();
},
/**
* Updates the elements that change regularly for in-progress downloads,
* namely the progress bar and the status line.
*/
_updateProgress() {
if (this.download.succeeded) {
// We only need to add or remove this attribute for succeeded downloads.
if (this.download.target.exists) {
this.element.setAttribute("exists", "true");
} else {
this.element.removeAttribute("exists");
}
}
// When a block is confirmed, the removal of blocked data will not trigger a
// state change for the download, so this class must be updated here.
this.element.classList.toggle("temporary-block",
!!this.download.hasBlockedData);
// The progress bar is only displayed for in-progress downloads.
if (this.download.hasProgress) {
this.element.setAttribute("progressmode", "normal");
this.element.setAttribute("progress", this.download.progress);
} else {
this.element.setAttribute("progressmode", "undetermined");
}
if (this.download.stopped && this.download.canceled &&
this.download.hasPartialData) {
this.element.setAttribute("progresspaused", "true");
} else {
this.element.removeAttribute("progresspaused");
}
// Dispatch the ValueChange event for accessibility, if possible.
if (this._progressElement) {
let event = this.element.ownerDocument.createEvent("Events");
event.initEvent("ValueChange", true, true);
this._progressElement.dispatchEvent(event);
}
let status = this.statusTextAndTip;
this.element.setAttribute("status", status.text);
this.element.setAttribute("statusTip", status.tip);
},
lastEstimatedSecondsLeft: Infinity,
/**
* Returns the text for the status line and the associated tooltip. These are
* returned by a single property because they are computed together. The
* result may be overridden by derived objects.
*/
get statusTextAndTip() {
return this.rawStatusTextAndTip;
},
/**
* Derived objects may call this to get the status text.
*/
get rawStatusTextAndTip() {
let s = DownloadsCommon.strings;
let text = "";
let tip = "";
if (!this.download.stopped) {
let totalBytes = this.download.hasProgress ? this.download.totalBytes
: -1;
// By default, extended status information including the individual
// download rate is displayed in the tooltip. The history view overrides
// the getter and displays the datails in the main area instead.
[text] = DownloadUtils.getDownloadStatusNoRate(
this.download.currentBytes,
totalBytes,
this.download.speed,
this.lastEstimatedSecondsLeft);
let newEstimatedSecondsLeft;
[tip, newEstimatedSecondsLeft] = DownloadUtils.getDownloadStatus(
this.download.currentBytes,
totalBytes,
this.download.speed,
this.lastEstimatedSecondsLeft);
this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
} else if (this.download.canceled && this.download.hasPartialData) {
let totalBytes = this.download.hasProgress ? this.download.totalBytes
: -1;
let transfer = DownloadUtils.getTransferTotal(this.download.currentBytes,
totalBytes);
// We use the same XUL label to display both the state and the amount
// transferred, for example "Paused - 1.1 MB".
text = s.statusSeparatorBeforeNumber(s.statePaused, transfer);
} else if (!this.download.succeeded && !this.download.canceled &&
!this.download.error) {
text = s.stateStarting;
} else {
let stateLabel;
if (this.download.succeeded) {
// For completed downloads, show the file size (e.g. "1.5 MB").
if (this.download.target.size !== undefined) {
let [size, unit] =
DownloadUtils.convertByteUnits(this.download.target.size);
stateLabel = s.sizeWithUnits(size, unit);
} else {
// History downloads may not have a size defined.
stateLabel = s.sizeUnknown;
}
} else if (this.download.canceled) {
stateLabel = s.stateCanceled;
} else if (this.download.error.becauseBlockedByParentalControls) {
stateLabel = s.stateBlockedParentalControls;
} else if (this.download.error.becauseBlockedByReputationCheck) {
stateLabel = this.rawBlockedTitleAndDetails[0];
} else {
stateLabel = s.stateFailed;
}
let referrer = this.download.source.referrer || this.download.source.url;
let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer);
let date = new Date(this.download.endTime);
let [displayDate, fullDate] = DownloadUtils.getReadableDates(date);
let firstPart = s.statusSeparator(stateLabel, displayHost);
text = s.statusSeparator(firstPart, displayDate);
tip = s.statusSeparator(fullHost, fullDate);
}
return { text, tip: tip || text };
},
/**
* Returns [title, [details1, details2]] for blocked downloads.
*/
get rawBlockedTitleAndDetails() {
let s = DownloadsCommon.strings;
if (!this.download.error ||
!this.download.error.becauseBlockedByReputationCheck) {
return [null, null];
}
switch (this.download.error.reputationCheckVerdict) {
case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
return [s.blockedUncommon2, [s.unblockTypeUncommon2, s.unblockTip2]];
case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
return [s.blockedPotentiallyUnwanted,
[s.unblockTypePotentiallyUnwanted2, s.unblockTip2]];
case Downloads.Error.BLOCK_VERDICT_MALWARE:
return [s.blockedMalware, [s.unblockTypeMalware, s.unblockTip2]];
}
throw new Error("Unexpected reputationCheckVerdict: " +
this.download.error.reputationCheckVerdict);
// return anyway to avoid a JS strict warning.
return [null, null];
},
/**
* Shows the appropriate unblock dialog based on the verdict, and executes the
* action selected by the user in the dialog, which may involve unblocking,
* opening or removing the file.
*
* @param window
* The window to which the dialog should be anchored.
* @param dialogType
* Can be "unblock", "chooseUnblock", or "chooseOpen".
*/
confirmUnblock(window, dialogType) {
DownloadsCommon.confirmUnblockDownload({
verdict: this.download.error.reputationCheckVerdict,
window,
dialogType,
}).then(action => {
if (action == "open") {
return this.unblockAndOpenDownload();
} else if (action == "unblock") {
return this.download.unblock();
} else if (action == "confirmBlock") {
return this.download.confirmBlock();
}
}).catch(Cu.reportError);
},
/**
* Unblocks the downloaded file and opens it.
*
* @return A promise that's resolved after the file has been opened.
*/
unblockAndOpenDownload() {
return this.download.unblock().then(() => this.downloadsCmd_open());
},
/**
* Returns the name of the default command to use for the current state of the
* download, when there is a double click or another default interaction. If
* there is no default command for the current state, returns an empty string.
* The commands are implemented as functions on this object or derived ones.
*/
get currentDefaultCommandName() {
switch (DownloadsCommon.stateOfDownload(this.download)) {
case Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED:
return "downloadsCmd_cancel";
case Ci.nsIDownloadManager.DOWNLOAD_FAILED:
case Ci.nsIDownloadManager.DOWNLOAD_CANCELED:
return "downloadsCmd_retry";
case Ci.nsIDownloadManager.DOWNLOAD_PAUSED:
return "downloadsCmd_pauseResume";
case Ci.nsIDownloadManager.DOWNLOAD_FINISHED:
return "downloadsCmd_open";
case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL:
return "downloadsCmd_openReferrer";
case Ci.nsIDownloadManager.DOWNLOAD_DIRTY:
return "downloadsCmd_showBlockedInfo";
}
return "";
},
/**
* Returns true if the specified command can be invoked on the current item.
* The commands are implemented as functions on this object or derived ones.
*
* @param aCommand
* Name of the command to check, for example "downloadsCmd_retry".
*/
isCommandEnabled(aCommand) {
switch (aCommand) {
case "downloadsCmd_retry":
return this.download.canceled || this.download.error;
case "downloadsCmd_pauseResume":
return this.download.hasPartialData && !this.download.error;
case "downloadsCmd_openReferrer":
return !!this.download.source.referrer;
case "downloadsCmd_confirmBlock":
case "downloadsCmd_chooseUnblock":
case "downloadsCmd_chooseOpen":
case "downloadsCmd_unblock":
case "downloadsCmd_unblockAndOpen":
return this.download.hasBlockedData;
}
return false;
},
downloadsCmd_cancel() {
// This is the correct way to avoid race conditions when cancelling.
this.download.cancel().catch(() => {});
this.download.removePartialData().catch(Cu.reportError);
},
downloadsCmd_retry() {
// Errors when retrying are already reported as download failures.
this.download.start().catch(() => {});
},
downloadsCmd_pauseResume() {
if (this.download.stopped) {
this.download.start();
} else {
this.download.cancel();
}
},
downloadsCmd_confirmBlock() {
this.download.confirmBlock().catch(Cu.reportError);
},
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,131 @@
<?xml version="1.0"?>
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml-stylesheet href="chrome://browser/content/downloads/downloads.css"?>
<?xml-stylesheet href="chrome://browser/skin/downloads/allDownloadsViewOverlay.css"?>
<!DOCTYPE overlay [
<!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/downloads/downloads.dtd">
%downloadsDTD;
]>
<!-- This overlay provides a downloads view that lists both session downloads,
using the DownloadsView API, and history downloads, using places queries.
The view also implements a command controller and a context menu for
managing the downloads list. In order to use this view:
1. Apply this overlay to your window.
2. Insert in all the overlay entry-points, namely:
<richlistbox id="downloadsRichListBox"/>
<commandset id="downloadCommands"/>
<menupopup id="downloadsContextMenu"/>
3. Make sure your window has the editMenuOverlay overlay applied,
because the view implements cmd_copy and cmd_delete.
4. Make sure your window has the globalOverlay.js script loaded.
5. To initialize the view
let view = new DownloadsPlacesView(document.getElementById("downloadsRichListBox"));
// This is what the Places Library uses. It could be tweaked a bit as long as the
// transition-type is set correctly
view.place = "place:transition=7&sort=4";
-->
<overlay id="downloadsViewOverlay"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript"
src="chrome://browser/content/downloads/allDownloadsViewOverlay.js"/>
<script type="application/javascript"
src="chrome://global/content/contentAreaUtils.js"/>
<richlistbox flex="1"
seltype="multiple"
id="downloadsRichListBox" context="downloadsContextMenu"
onscroll="return this._placesView.onScroll();"
onkeypress="return this._placesView.onKeyPress(event);"
ondblclick="return this._placesView.onDoubleClick(event);"
oncontextmenu="return this._placesView.onContextMenu(event);"
ondragstart="this._placesView.onDragStart(event);"
ondragover="this._placesView.onDragOver(event);"
ondrop="this._placesView.onDrop(event);"
onfocus="goUpdateDownloadCommands();"
onselect="this._placesView.onSelect();"
onblur="goUpdateDownloadCommands();"/>
<commandset id="downloadCommands"
commandupdater="true"
events="focus,select,contextmenu"
oncommandupdate="goUpdateDownloadCommands();">
<command id="downloadsCmd_pauseResume"
oncommand="goDoCommand('downloadsCmd_pauseResume')"/>
<command id="downloadsCmd_cancel"
oncommand="goDoCommand('downloadsCmd_cancel')"/>
<command id="downloadsCmd_unblock"
oncommand="goDoCommand('downloadsCmd_unblock')"/>
<command id="downloadsCmd_chooseUnblock"
oncommand="goDoCommand('downloadsCmd_chooseUnblock')"/>
<command id="downloadsCmd_chooseOpen"
oncommand="goDoCommand('downloadsCmd_chooseOpen')"/>
<command id="downloadsCmd_confirmBlock"
oncommand="goDoCommand('downloadsCmd_confirmBlock')"/>
<command id="downloadsCmd_open"
oncommand="goDoCommand('downloadsCmd_open')"/>
<command id="downloadsCmd_show"
oncommand="goDoCommand('downloadsCmd_show')"/>
<command id="downloadsCmd_retry"
oncommand="goDoCommand('downloadsCmd_retry')"/>
<command id="downloadsCmd_openReferrer"
oncommand="goDoCommand('downloadsCmd_openReferrer')"/>
<command id="downloadsCmd_clearDownloads"
oncommand="goDoCommand('downloadsCmd_clearDownloads')"/>
</commandset>
<menupopup id="downloadsContextMenu" class="download-state">
<menuitem command="downloadsCmd_pauseResume"
class="downloadPauseMenuItem"
label="&cmd.pause.label;"
accesskey="&cmd.pause.accesskey;"/>
<menuitem command="downloadsCmd_pauseResume"
class="downloadResumeMenuItem"
label="&cmd.resume.label;"
accesskey="&cmd.resume.accesskey;"/>
<menuitem command="downloadsCmd_cancel"
class="downloadCancelMenuItem"
label="&cmd.cancel.label;"
accesskey="&cmd.cancel.accesskey;"/>
<menuitem command="downloadsCmd_unblock"
class="downloadUnblockMenuItem"
label="&cmd.unblock2.label;"
accesskey="&cmd.unblock2.accesskey;"/>
<menuitem command="cmd_delete"
class="downloadRemoveFromHistoryMenuItem"
label="&cmd.removeFromHistory.label;"
accesskey="&cmd.removeFromHistory.accesskey;"/>
<menuitem command="downloadsCmd_show"
class="downloadShowMenuItem"
#ifdef XP_MACOSX
label="&cmd.showMac.label;"
accesskey="&cmd.showMac.accesskey;"
#else
label="&cmd.show.label;"
accesskey="&cmd.show.accesskey;"
#endif
/>
<menuseparator class="downloadCommandsSeparator"/>
<menuitem command="downloadsCmd_openReferrer"
label="&cmd.goToDownloadPage.label;"
accesskey="&cmd.goToDownloadPage.accesskey;"/>
<menuitem command="cmd_copy"
label="&cmd.copyDownloadLink.label;"
accesskey="&cmd.copyDownloadLink.accesskey;"/>
<menuseparator/>
<menuitem command="downloadsCmd_clearDownloads"
label="&cmd.clearDownloads.label;"
accesskey="&cmd.clearDownloads.accesskey;"/>
</menupopup>
</overlay>

View file

@ -0,0 +1,11 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#downloadsListEmptyDescription {
display: none;
}
#downloadsRichListBox:empty + #downloadsListEmptyDescription {
display: -moz-box;
}

View file

@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
var ContentAreaDownloadsView = {
init() {
let view = new DownloadsPlacesView(document.getElementById("downloadsRichListBox"));
// Do not display the Places downloads in private windows
if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) {
view.place = "place:transition=7&sort=4";
}
// Set focus to Downloads list once it is created
document.getElementById("downloadsRichListBox").focus();
},
};

View file

@ -0,0 +1,46 @@
<?xml version="1.0"?>
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://browser/content/downloads/contentAreaDownloadsView.css"?>
<?xml-stylesheet href="chrome://browser/skin/downloads/contentAreaDownloadsView.css"?>
<?xul-overlay href="chrome://browser/content/downloads/allDownloadsViewOverlay.xul"?>
<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
<!DOCTYPE window [
<!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/downloads/downloads.dtd">
%downloadsDTD;
]>
<window id="contentAreaDownloadsView"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="&downloads.title;"
onload="ContentAreaDownloadsView.init();">
<script type="application/javascript"
src="chrome://global/content/globalOverlay.js"/>
<script type="application/javascript"
src="chrome://browser/content/downloads/contentAreaDownloadsView.js"/>
<commandset id="editMenuCommands"/>
<keyset id="editMenuKeys">
#ifdef XP_MACOSX
<key id="key_delete2" keycode="VK_BACK" command="cmd_delete"/>
#endif
</keyset>
<stack flex="1">
<richlistbox id="downloadsRichListBox"/>
<description id="downloadsListEmptyDescription"
value="&downloadsListEmpty.label;"
mousethrough="always"/>
</stack>
<commandset id="downloadCommands"/>
<menupopup id="downloadsContextMenu"/>
</window>

View file

@ -0,0 +1,99 @@
<?xml version="1.0"?>
<!-- -*- Mode: HTML; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- -->
<!-- vim: set ts=2 et sw=2 tw=80: -->
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
- You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE bindings SYSTEM "chrome://browser/locale/downloads/downloads.dtd">
<bindings id="downloadBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="download"
extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
<content orient="horizontal"
onclick="DownloadsView.onDownloadClick(event);">
<xul:hbox class="downloadMainArea"
flex="1"
align="center">
<xul:stack>
<xul:image class="downloadTypeIcon"
validate="always"
xbl:inherits="src=image"/>
<xul:image class="downloadBlockedBadge" />
</xul:stack>
<xul:vbox pack="center"
flex="1"
class="downloadContainer"
style="width: &downloadDetails.width;">
<!-- We're letting localizers put a min-width in here primarily
because of the downloads summary at the bottom of the list of
download items. An element in the summary has the same min-width
on a description, and we don't want the panel to change size if the
summary isn't being displayed, so we ensure that items share the
same minimum width.
-->
<xul:description class="downloadTarget"
crop="center"
style="min-width: &downloadsSummary.minWidth2;"
xbl:inherits="value=displayName,tooltiptext=displayName"/>
<xul:progressmeter anonid="progressmeter"
class="downloadProgress"
min="0"
max="100"
xbl:inherits="mode=progressmode,value=progress,paused=progresspaused"/>
<xul:description class="downloadDetails"
crop="end"
xbl:inherits="value=status,tooltiptext=statusTip"/>
</xul:vbox>
</xul:hbox>
<xul:toolbarseparator />
<xul:stack class="downloadButtonArea">
<xul:button class="downloadButton downloadCancel downloadIconCancel"
tooltiptext="&cmd.cancel.label;"
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_cancel');"/>
<xul:button class="downloadButton downloadRetry downloadIconRetry"
tooltiptext="&cmd.retry.label;"
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_retry');"/>
<xul:button class="downloadButton downloadShow downloadIconShow"
#ifdef XP_MACOSX
tooltiptext="&cmd.showMac.label;"
#else
tooltiptext="&cmd.show.label;"
#endif
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_show');"/>
<xul:button class="downloadButton downloadConfirmBlock downloadIconCancel"
tooltiptext="&cmd.removeFile.label;"
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_confirmBlock');"/>
<xul:button class="downloadButton downloadChooseUnblock downloadIconShow"
tooltiptext="&cmd.chooseUnblock.label;"
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_chooseUnblock');"/>
<xul:button class="downloadButton downloadChooseOpen downloadIconShow"
tooltiptext="&cmd.chooseOpen.label;"
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_chooseOpen');"/>
<xul:button class="downloadButton downloadShowBlockedInfo"
tooltiptext="&cmd.chooseUnblock.label;"
oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_showBlockedInfo');"/>
</xul:stack>
</content>
</binding>
<binding id="download-toolbarbutton"
extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-badged">
<content>
<xul:stack class="toolbarbutton-badge-stack">
<children />
<xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/>
<xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0" crop="none"/>
</xul:stack>
<xul:label class="toolbarbutton-text" crop="right" flex="1"
xbl:inherits="value=label,accesskey,crop,wrap"/>
<xul:label class="toolbarbutton-multiline-text" flex="1"
xbl:inherits="xbl:text=label,accesskey,wrap"/>
</content>
</binding>
</bindings>

View file

@ -0,0 +1,267 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/*** Downloads Panel ***/
richlistitem[type="download"] {
-moz-binding: url('chrome://browser/content/downloads/download.xml#download');
}
richlistitem[type="download"]:not([selected]) button {
/* Only focus buttons in the selected item. */
-moz-user-focus: none;
}
.downloadsHideDropmarker > #downloadsFooterButtonsSplitter,
.downloadsHideDropmarker > #downloadsFooterDropmarker {
display: none;
}
richlistitem[type="download"].download-state[state="1"]:not([exists]) > .downloadButtonArea,
richlistitem[type="download"].download-state[state="1"]:not([exists]) > toolbarseparator {
display: none;
}
#downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryProgress,
#downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryDetails,
#downloadsFooter:not([showingsummary]) #downloadsSummary {
display: none;
}
#downloadsFooter[showingdropdown] > stack > #downloadsSummary,
#downloadsFooter[showingsummary] > stack:hover > #downloadsSummary,
#downloadsFooter[showingsummary]:not([showingdropdown]) > stack:not(:hover) > #downloadsFooterButtons {
/* If we used "visibility: hidden;" then the mouseenter event of
#downloadsHistory wouldn't be triggered immediately, and the hover styling
of the button would not apply until the mouse is moved again.
"-moz-user-focus: ignore;" prevents the elements with "opacity: 0;" from
being focused with the keyboard. */
opacity: 0;
-moz-user-focus: ignore;
}
/*** Downloads View ***/
/**
* The downloads richlistbox may list thousands of items, and it turns out
* XBL binding attachment, and even more so detachment, is a performance hog.
* This hack makes sure we don't apply any binding to inactive items (inactive
* items are history downloads that haven't been in the visible area).
* We can do this because the richlistbox implementation does not interact
* much with the richlistitem binding. However, this may turn out to have
* some side effects (see bug 828111 for the details).
*
* We might be able to do away with this workaround once bug 653881 is fixed.
*/
richlistitem.download {
-moz-binding: none;
}
richlistitem.download[active] {
-moz-binding: url("chrome://browser/content/downloads/download.xml#download");
}
richlistitem.download button {
/* These buttons should never get focus, as that would "disable"
the downloads view controller (it's only used when the richlistbox
is focused). */
-moz-user-focus: none;
}
/*** Visibility of controls inside download items ***/
.download-state:not(:-moz-any([state="6"], /* Blocked (parental) */
[state="8"], /* Blocked (dirty) */
[state="9"]) /* Blocked (policy) */)
.downloadBlockedBadge,
.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */
[state="5"], /* Starting (queued) */
[state="0"], /* Downloading */
[state="4"], /* Paused */
[state="7"]) /* Scanning */)
.downloadProgress,
.download-state:not( [state="0"] /* Downloading */)
.downloadPauseMenuItem,
.download-state:not( [state="4"] /* Paused */)
.downloadResumeMenuItem,
/* Blocked (dirty) downloads that have not been confirmed and
have temporary data. */
.download-state:not( [state="8"] /* Blocked (dirty) */)
.downloadUnblockMenuItem,
.download-state[state="8"]:not(.temporary-block)
.downloadUnblockMenuItem,
.download-state:not(:-moz-any([state="2"], /* Failed */
[state="4"]) /* Paused */)
.downloadCancelMenuItem,
.download-state:not(:-moz-any([state="1"], /* Finished */
[state="2"], /* Failed */
[state="3"], /* Canceled */
[state="6"], /* Blocked (parental) */
[state="8"], /* Blocked (dirty) */
[state="9"]) /* Blocked (policy) */)
.downloadRemoveFromHistoryMenuItem,
.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */
[state="0"], /* Downloading */
[state="1"], /* Finished */
[state="4"], /* Paused */
[state="5"]) /* Starting (queued) */)
.downloadShowMenuItem,
.download-state[state="7"] .downloadCommandsSeparator
{
display: none;
}
/*** Visibility of download buttons ***/
.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */
[state="5"], /* Starting (queued) */
[state="0"], /* Downloading */
[state="4"]) /* Paused */)
.downloadCancel,
/* Blocked (dirty) downloads that have not been confirmed and
have temporary data, for the Malware case. */
.download-state:not( [state="8"] /* Blocked (dirty) */)
.downloadConfirmBlock,
.download-state[state="8"]:not(.temporary-block)
.downloadConfirmBlock,
.download-state[state="8"].temporary-block:not([verdict="Malware"])
.downloadConfirmBlock,
/* Blocked (dirty) downloads that have not been confirmed and
have temporary data, for the Potentially Unwanted case. */
.download-state:not( [state="8"] /* Blocked (dirty) */)
.downloadChooseUnblock,
.download-state[state="8"]:not(.temporary-block)
.downloadChooseUnblock,
.download-state[state="8"].temporary-block:not([verdict="PotentiallyUnwanted"])
.downloadChooseUnblock,
/* Blocked (dirty) downloads that have not been confirmed and
have temporary data, for the Uncommon case. */
.download-state:not( [state="8"] /* Blocked (dirty) */)
.downloadChooseOpen,
.download-state[state="8"]:not(.temporary-block)
.downloadChooseOpen,
.download-state[state="8"].temporary-block:not([verdict="Uncommon"])
.downloadChooseOpen,
.download-state:not(:-moz-any([state="2"], /* Failed */
[state="3"]) /* Canceled */)
.downloadRetry,
.download-state:not( [state="1"] /* Finished */)
.downloadShow,
.download-state:-moz-any( [state="6"], /* Blocked (parental) */
[state="7"], /* Scanning */
[state="9"]) /* Blocked (policy) */
> toolbarseparator,
/* The "show blocked info" button is shown only in the downloads panel. */
.downloadShowBlockedInfo
{
display: none;
}
/*** Downloads panel ***/
#downloadsPanel[hasdownloads] #emptyDownloads,
#downloadsPanel:not([hasdownloads]) #downloadsListBox {
display: none;
}
/*** Downloads panel multiview (main view and blocked-downloads subview) ***/
/* Hide all the usual buttons. */
#downloadsPanel-mainView .download-state[state="8"] .downloadCancel,
#downloadsPanel-mainView .download-state[state="8"] .downloadConfirmBlock,
#downloadsPanel-mainView .download-state[state="8"] .downloadChooseUnblock,
#downloadsPanel-mainView .download-state[state="8"] .downloadChooseOpen,
#downloadsPanel-mainView .download-state[state="8"] .downloadRetry,
#downloadsPanel-mainView .download-state[state="8"] .downloadShow {
display: none;
}
/* Make the panel wide enough to show the download list items without improperly
truncating them. */
#downloadsPanel-multiView > .panel-viewcontainer,
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack,
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack > .panel-mainview {
max-width: unset;
}
/* Show the "show blocked info" button. */
#downloadsPanel-mainView .download-state[state="8"] .downloadShowBlockedInfo {
display: inline;
}
/** When the main view is showing... **/
/* The subview should be off to the right and not visible at all. */
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype=main] > .panel-subviews {
transform: translateX(101%);
transition: transform var(--panelui-subview-transition-duration);
}
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype=main] > .panel-subviews:-moz-locale-dir(rtl) {
transform: translateX(-101%);
}
/** When the subview is showing... **/
/* Hide the buttons of all downloads except the one that triggered the
subview. */
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] .download-state:not([showingsubview]) .downloadButton {
visibility: hidden;
}
/* For the download that triggered the subview, move its button farther to the
right by removing padding so that a minimum amount of the main view's right
edge needs to be shown. */
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] .download-state[showingsubview] {
padding: 0;
}
/* The main view should slide to the left and its right edge should remain
visible. */
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype=subview] > .panel-mainview {
transform: translateX(calc(-100% + 38px));
transition: transform var(--panelui-subview-transition-duration);
}
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype=subview] > .panel-mainview:-moz-locale-dir(rtl) {
transform: translateX(calc(100% - 38px));
}
/* The subview should leave the right edge of the main view uncovered. */
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack > .panel-subviews {
/* Use a margin instead of a transform like above so that the subview's width
isn't wider than the panel. */
-moz-margin-start: 38px !important;
}
/* Prevent keyboard interaction in the main view by preventing all elements in
the main view from being focused... */
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview #downloadsListBox,
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview richlistitem,
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview .downloadButton,
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview .downloadsPanelFooterButton,
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview #downloadsSummary {
-moz-user-focus: ignore;
}
/* ... except for the downloadShowBlockedInfo button in the blocked download.
Selecting it with the keyboard should show the main view again. */
#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] .download-state[showingsubview] .downloadShowBlockedInfo {
-moz-user-focus: normal;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,210 @@
<?xml version="1.0"?>
# -*- Mode: HTML; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
# vim: set ts=2 et sw=2 tw=80:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
<?xml-stylesheet href="chrome://browser/content/downloads/downloads.css"?>
<?xml-stylesheet href="chrome://browser/skin/downloads/downloads.css"?>
<!DOCTYPE overlay SYSTEM "chrome://browser/locale/downloads/downloads.dtd">
<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
id="downloadsOverlay">
<commandset>
<command id="downloadsCmd_doDefault"
oncommand="goDoCommand('downloadsCmd_doDefault')"/>
<command id="downloadsCmd_pauseResume"
oncommand="goDoCommand('downloadsCmd_pauseResume')"/>
<command id="downloadsCmd_cancel"
oncommand="goDoCommand('downloadsCmd_cancel')"/>
<command id="downloadsCmd_unblock"
oncommand="goDoCommand('downloadsCmd_unblock')"/>
<command id="downloadsCmd_chooseUnblock"
oncommand="goDoCommand('downloadsCmd_chooseUnblock')"/>
<command id="downloadsCmd_unblockAndOpen"
oncommand="goDoCommand('downloadsCmd_unblockAndOpen')"/>
<command id="downloadsCmd_confirmBlock"
oncommand="goDoCommand('downloadsCmd_confirmBlock')"/>
<command id="downloadsCmd_open"
oncommand="goDoCommand('downloadsCmd_open')"/>
<command id="downloadsCmd_show"
oncommand="goDoCommand('downloadsCmd_show')"/>
<command id="downloadsCmd_retry"
oncommand="goDoCommand('downloadsCmd_retry')"/>
<command id="downloadsCmd_openReferrer"
oncommand="goDoCommand('downloadsCmd_openReferrer')"/>
<command id="downloadsCmd_copyLocation"
oncommand="goDoCommand('downloadsCmd_copyLocation')"/>
<command id="downloadsCmd_clearList"
oncommand="goDoCommand('downloadsCmd_clearList')"/>
</commandset>
<popupset id="mainPopupSet">
<!-- The panel has level="top" to ensure that it is never hidden by the
taskbar on Windows. See bug 672365. For accessibility to screen
readers, we use a label on the panel instead of the anchor because the
panel can also be displayed without an anchor. -->
<panel id="downloadsPanel"
aria-label="&downloads.title;"
role="group"
type="arrow"
orient="vertical"
level="top"
onpopupshown="DownloadsPanel.onPopupShown(event);"
onpopuphidden="DownloadsPanel.onPopupHidden(event);">
<!-- The following popup menu should be a child of the panel element,
otherwise flickering may occur when the cursor is moved over the area
of a disabled menu item that overlaps the panel. See bug 492960. -->
<menupopup id="downloadsContextMenu"
onpopupshown="DownloadsView.onContextPopupShown(event);"
onpopuphidden="DownloadsView.onContextPopupHidden(event);"
class="download-state">
<menuitem command="downloadsCmd_pauseResume"
class="downloadPauseMenuItem"
label="&cmd.pause.label;"
accesskey="&cmd.pause.accesskey;"/>
<menuitem command="downloadsCmd_pauseResume"
class="downloadResumeMenuItem"
label="&cmd.resume.label;"
accesskey="&cmd.resume.accesskey;"/>
<menuitem command="downloadsCmd_cancel"
class="downloadCancelMenuItem"
label="&cmd.cancel.label;"
accesskey="&cmd.cancel.accesskey;"/>
<menuitem command="downloadsCmd_unblock"
class="downloadUnblockMenuItem"
label="&cmd.unblock2.label;"
accesskey="&cmd.unblock2.accesskey;"/>
<menuitem command="cmd_delete"
class="downloadRemoveFromHistoryMenuItem"
label="&cmd.removeFromHistory.label;"
accesskey="&cmd.removeFromHistory.accesskey;"/>
<menuitem command="downloadsCmd_show"
class="downloadShowMenuItem"
#ifdef XP_MACOSX
label="&cmd.showMac.label;"
accesskey="&cmd.showMac.accesskey;"
#else
label="&cmd.show.label;"
accesskey="&cmd.show.accesskey;"
#endif
/>
<menuseparator class="downloadCommandsSeparator"/>
<menuitem command="downloadsCmd_openReferrer"
label="&cmd.goToDownloadPage.label;"
accesskey="&cmd.goToDownloadPage.accesskey;"/>
<menuitem command="downloadsCmd_copyLocation"
label="&cmd.copyDownloadLink.label;"
accesskey="&cmd.copyDownloadLink.accesskey;"/>
<menuseparator/>
<menuitem command="downloadsCmd_clearList"
label="&cmd.clearList2.label;"
accesskey="&cmd.clearList2.accesskey;"/>
</menupopup>
<panelmultiview id="downloadsPanel-multiView"
mainViewId="downloadsPanel-mainView"
align="stretch">
<panelview id="downloadsPanel-mainView"
flex="1"
align="stretch">
<richlistbox id="downloadsListBox"
context="downloadsContextMenu"
onmouseover="DownloadsView.onDownloadMouseOver(event);"
onmouseout="DownloadsView.onDownloadMouseOut(event);"
oncontextmenu="DownloadsView.onDownloadContextMenu(event);"
ondragstart="DownloadsView.onDownloadDragStart(event);"/>
<description id="emptyDownloads"
mousethrough="always">
&downloadsPanelEmpty.label;
</description>
<spacer flex="1"/>
<vbox id="downloadsFooter"
class="downloadsPanelFooter">
<stack>
<hbox id="downloadsSummary"
align="center"
orient="horizontal"
onkeydown="DownloadsSummary.onKeyDown(event);"
onclick="DownloadsSummary.onClick(event);">
<image class="downloadTypeIcon" />
<vbox pack="center"
class="downloadContainer"
style="width: &downloadDetails.width;">
<description id="downloadsSummaryDescription"
style="min-width: &downloadsSummary.minWidth2;"/>
<progressmeter id="downloadsSummaryProgress"
class="downloadProgress"
min="0"
max="100"
mode="normal" />
<description id="downloadsSummaryDetails"
crop="end"/>
</vbox>
</hbox>
<hbox id="downloadsFooterButtons">
<button id="downloadsHistory"
class="downloadsPanelFooterButton"
label="&downloadsHistory.label;"
accesskey="&downloadsHistory.accesskey;"
flex="1"
oncommand="DownloadsPanel.showDownloadsHistory();"/>
<toolbarseparator id="downloadsFooterButtonsSplitter"
class="downloadsDropmarkerSplitter"/>
<button id="downloadsFooterDropmarker"
class="downloadsPanelFooterButton downloadsDropmarker"
type="menu">
<menupopup id="downloadSubPanel"
onpopupshowing="DownloadsPanel.onFooterPopupShowing(event);"
onpopuphidden="DownloadsPanel.onFooterPopupHidden(event);"
position="after_end">
<menuitem id="downloadsDropdownItemClearList"
command="downloadsCmd_clearList"
label="&cmd.clearList2.label;"/>
<menuitem id="downloadsDropdownItemOpenDownloadsFolder"
oncommand="DownloadsPanel.openDownloadsFolder();"
label="&openDownloadsFolder.label;"/>
</menupopup>
</button>
</hbox>
</stack>
</vbox>
</panelview>
<panelview id="downloadsPanel-blockedSubview"
orient="vertical"
flex="1">
<description id="downloadsPanel-blockedSubview-title"/>
<description id="downloadsPanel-blockedSubview-details1"/>
<description id="downloadsPanel-blockedSubview-details2"/>
<spacer flex="1"/>
<hbox id="downloadsPanel-blockedSubview-buttons"
class="downloadsPanelFooter"
align="stretch">
<button id="downloadsPanel-blockedSubview-openButton"
class="downloadsPanelFooterButton"
command="downloadsCmd_unblockAndOpen"
flex="1"/>
<toolbarseparator/>
<button id="downloadsPanel-blockedSubview-deleteButton"
class="downloadsPanelFooterButton"
oncommand="DownloadsBlockedSubview.confirmBlock();"
default="true"
flex="1"/>
</hbox>
</panelview>
</panelmultiview>
</panel>
</popupset>
</overlay>

View file

@ -0,0 +1,606 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Handles the indicator that displays the progress of ongoing downloads, which
* is also used as the anchor for the downloads panel.
*
* This module includes the following constructors and global objects:
*
* DownloadsButton
* Main entry point for the downloads indicator. Depending on how the toolbars
* have been customized, this object determines if we should show a fully
* functional indicator, a placeholder used during customization and in the
* customization palette, or a neutral view as a temporary anchor for the
* downloads panel.
*
* DownloadsIndicatorView
* Builds and updates the actual downloads status widget, responding to changes
* in the global status data, or provides a neutral view if the indicator is
* removed from the toolbars and only used as a temporary anchor. In addition,
* handles the user interaction events raised by the widget.
*/
"use strict";
////////////////////////////////////////////////////////////////////////////////
//// DownloadsButton
/**
* Main entry point for the downloads indicator. Depending on how the toolbars
* have been customized, this object determines if we should show a fully
* functional indicator, a placeholder used during customization and in the
* customization palette, or a neutral view as a temporary anchor for the
* downloads panel.
*/
const DownloadsButton = {
/**
* Location of the indicator overlay.
*/
get kIndicatorOverlay() {
return "chrome://browser/content/downloads/indicatorOverlay.xul";
},
/**
* Returns a reference to the downloads button position placeholder, or null
* if not available because it has been removed from the toolbars.
*/
get _placeholder() {
return document.getElementById("downloads-button");
},
/**
* This function is called asynchronously just after window initialization.
*
* NOTE: This function should limit the input/output it performs to improve
* startup time.
*/
initializeIndicator() {
DownloadsIndicatorView.ensureInitialized();
},
/**
* Indicates whether toolbar customization is in progress.
*/
_customizing: false,
/**
* This function is called when toolbar customization starts.
*
* During customization, we never show the actual download progress indication
* or the event notifications, but we show a neutral placeholder. The neutral
* placeholder is an ordinary button defined in the browser window that can be
* moved freely between the toolbars and the customization palette.
*/
customizeStart() {
// Prevent the indicator from being displayed as a temporary anchor
// during customization, even if requested using the getAnchor method.
this._customizing = true;
this._anchorRequested = false;
},
/**
* This function is called when toolbar customization ends.
*/
customizeDone() {
this._customizing = false;
DownloadsIndicatorView.afterCustomize();
},
/**
* Determines the position where the indicator should appear, and moves its
* associated element to the new position.
*
* @return Anchor element, or null if the indicator is not visible.
*/
_getAnchorInternal() {
let indicator = DownloadsIndicatorView.indicator;
if (!indicator) {
// Exit now if the indicator overlay isn't loaded yet, or if the button
// is not in the document.
return null;
}
indicator.open = this._anchorRequested;
let widget = CustomizableUI.getWidget("downloads-button")
.forWindow(window);
// Determine if the indicator is located on an invisible toolbar.
if (!isElementVisible(indicator.parentNode) && !widget.overflowed) {
return null;
}
return DownloadsIndicatorView.indicatorAnchor;
},
/**
* Checks whether the indicator is, or will soon be visible in the browser
* window.
*
* @param aCallback
* Called once the indicator overlay has loaded. Gets a boolean
* argument representing the indicator visibility.
*/
checkIsVisible(aCallback) {
DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, () => {
if (!this._placeholder) {
aCallback(false);
} else {
let element = DownloadsIndicatorView.indicator || this._placeholder;
aCallback(isElementVisible(element.parentNode));
}
});
},
/**
* Indicates whether we should try and show the indicator temporarily as an
* anchor for the panel, even if the indicator would be hidden by default.
*/
_anchorRequested: false,
/**
* Ensures that there is an anchor available for the panel.
*
* @param aCallback
* Called when the anchor is available, passing the element where the
* panel should be anchored, or null if an anchor is not available (for
* example because both the tab bar and the navigation bar are hidden).
*/
getAnchor(aCallback) {
// Do not allow anchoring the panel to the element while customizing.
if (this._customizing) {
aCallback(null);
return;
}
DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, () => {
this._anchorRequested = true;
aCallback(this._getAnchorInternal());
});
},
/**
* Allows the temporary anchor to be hidden.
*/
releaseAnchor() {
this._anchorRequested = false;
this._getAnchorInternal();
},
get _tabsToolbar() {
delete this._tabsToolbar;
return this._tabsToolbar = document.getElementById("TabsToolbar");
},
get _navBar() {
delete this._navBar;
return this._navBar = document.getElementById("nav-bar");
}
};
Object.defineProperty(this, "DownloadsButton", {
value: DownloadsButton,
enumerable: true,
writable: false
});
////////////////////////////////////////////////////////////////////////////////
//// DownloadsIndicatorView
/**
* Builds and updates the actual downloads status widget, responding to changes
* in the global status data, or provides a neutral view if the indicator is
* removed from the toolbars and only used as a temporary anchor. In addition,
* handles the user interaction events raised by the widget.
*/
const DownloadsIndicatorView = {
/**
* True when the view is connected with the underlying downloads data.
*/
_initialized: false,
/**
* True when the user interface elements required to display the indicator
* have finished loading in the browser window, and can be referenced.
*/
_operational: false,
/**
* Prepares the downloads indicator to be displayed.
*/
ensureInitialized() {
if (this._initialized) {
return;
}
this._initialized = true;
window.addEventListener("unload", this.onWindowUnload, false);
DownloadsCommon.getIndicatorData(window).addView(this);
},
/**
* Frees the internal resources related to the indicator.
*/
ensureTerminated() {
if (!this._initialized) {
return;
}
this._initialized = false;
window.removeEventListener("unload", this.onWindowUnload, false);
DownloadsCommon.getIndicatorData(window).removeView(this);
// Reset the view properties, so that a neutral indicator is displayed if we
// are visible only temporarily as an anchor.
this.counter = "";
this.percentComplete = 0;
this.paused = false;
this.attention = DownloadsCommon.ATTENTION_NONE;
},
/**
* Ensures that the user interface elements required to display the indicator
* are loaded, then invokes the given callback.
*/
_ensureOperational(aCallback) {
if (this._operational) {
if (aCallback) {
aCallback();
}
return;
}
// If we don't have a _placeholder, there's no chance that the overlay
// will load correctly: bail (and don't set _operational to true!)
if (!DownloadsButton._placeholder) {
return;
}
DownloadsOverlayLoader.ensureOverlayLoaded(
DownloadsButton.kIndicatorOverlay,
() => {
this._operational = true;
// If the view is initialized, we need to update the elements now that
// they are finally available in the document.
if (this._initialized) {
DownloadsCommon.getIndicatorData(window).refreshView(this);
}
if (aCallback) {
aCallback();
}
});
},
//////////////////////////////////////////////////////////////////////////////
//// Direct control functions
/**
* Set while we are waiting for a notification to fade out.
*/
_notificationTimeout: null,
/**
* Check if the panel containing aNode is open.
* @param aNode
* the node whose panel we're interested in.
*/
_isAncestorPanelOpen(aNode) {
while (aNode && aNode.localName != "panel") {
aNode = aNode.parentNode;
}
return aNode && aNode.state == "open";
},
/**
* If the status indicator is visible in its assigned position, shows for a
* brief time a visual notification of a relevant event, like a new download.
*
* @param aType
* Set to "start" for new downloads, "finish" for completed downloads.
*/
showEventNotification(aType) {
if (!this._initialized) {
return;
}
if (!DownloadsCommon.animateNotifications) {
return;
}
// No need to show visual notification if the panel is visible.
if (DownloadsPanel.isPanelShowing) {
return;
}
let anchor = DownloadsButton._placeholder;
let widgetGroup = CustomizableUI.getWidget("downloads-button");
let widget = widgetGroup.forWindow(window);
if (widget.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
if (anchor && this._isAncestorPanelOpen(anchor)) {
// If the containing panel is open, don't do anything, because the
// notification would appear under the open panel. See
// https://bugzilla.mozilla.org/show_bug.cgi?id=984023
return;
}
// Otherwise, try to use the anchor of the panel:
anchor = widget.anchor;
}
if (!anchor || !isElementVisible(anchor.parentNode)) {
// Our container isn't visible, so can't show the animation:
return;
}
if (this._notificationTimeout) {
clearTimeout(this._notificationTimeout);
}
// The notification element is positioned to show in the same location as
// the downloads button. It's not in the downloads button itself in order to
// be able to anchor the notification elsewhere if required, and to ensure
// the notification isn't clipped by overflow properties of the anchor's
// container.
let notifier = this.notifier;
if (notifier.style.transform == '') {
let anchorRect = anchor.getBoundingClientRect();
let notifierRect = notifier.getBoundingClientRect();
let topDiff = anchorRect.top - notifierRect.top;
let leftDiff = anchorRect.left - notifierRect.left;
let heightDiff = anchorRect.height - notifierRect.height;
let widthDiff = anchorRect.width - notifierRect.width;
let translateX = (leftDiff + .5 * widthDiff) + "px";
let translateY = (topDiff + .5 * heightDiff) + "px";
notifier.style.transform = "translate(" + translateX + ", " + translateY + ")";
}
notifier.setAttribute("notification", aType);
this._notificationTimeout = setTimeout(() => {
notifier.removeAttribute("notification");
notifier.style.transform = '';
}, 1000);
},
//////////////////////////////////////////////////////////////////////////////
//// Callback functions from DownloadsIndicatorData
/**
* Indicates whether the indicator should be shown because there are some
* downloads to be displayed.
*/
set hasDownloads(aValue) {
if (this._hasDownloads != aValue || (!this._operational && aValue)) {
this._hasDownloads = aValue;
// If there is at least one download, ensure that the view elements are
if (aValue) {
this._ensureOperational();
}
}
return aValue;
},
get hasDownloads() {
return this._hasDownloads;
},
_hasDownloads: false,
/**
* Status text displayed in the indicator. If this is set to an empty value,
* then the small downloads icon is displayed instead of the text.
*/
set counter(aValue) {
if (!this._operational) {
return this._counter;
}
if (this._counter !== aValue) {
this._counter = aValue;
if (this._counter)
this.indicator.setAttribute("counter", "true");
else
this.indicator.removeAttribute("counter");
// We have to set the attribute instead of using the property because the
// XBL binding isn't applied if the element is invisible for any reason.
this._indicatorCounter.setAttribute("value", aValue);
}
return aValue;
},
_counter: null,
/**
* Progress indication to display, from 0 to 100, or -1 if unknown. The
* progress bar is hidden if the current progress is unknown and no status
* text is set in the "counter" property.
*/
set percentComplete(aValue) {
if (!this._operational) {
return this._percentComplete;
}
if (this._percentComplete !== aValue) {
this._percentComplete = aValue;
if (this._percentComplete >= 0)
this.indicator.setAttribute("progress", "true");
else
this.indicator.removeAttribute("progress");
// We have to set the attribute instead of using the property because the
// XBL binding isn't applied if the element is invisible for any reason.
this._indicatorProgress.setAttribute("value", Math.max(aValue, 0));
}
return aValue;
},
_percentComplete: null,
/**
* Indicates whether the progress won't advance because of a paused state.
* Setting this property forces a paused progress bar to be displayed, even if
* the current progress information is unavailable.
*/
set paused(aValue) {
if (!this._operational) {
return this._paused;
}
if (this._paused != aValue) {
this._paused = aValue;
if (this._paused) {
this.indicator.setAttribute("paused", "true")
} else {
this.indicator.removeAttribute("paused");
}
}
return aValue;
},
_paused: false,
/**
* Set when the indicator should draw user attention to itself.
*/
set attention(aValue) {
if (!this._operational) {
return this._attention;
}
if (this._attention != aValue) {
this._attention = aValue;
// Check if the downloads button is in the menu panel, to determine which
// button needs to get a badge.
let widgetGroup = CustomizableUI.getWidget("downloads-button");
let inMenu = widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL;
if (aValue == DownloadsCommon.ATTENTION_NONE) {
this.indicator.removeAttribute("attention");
if (inMenu) {
gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD);
}
} else {
this.indicator.setAttribute("attention", aValue);
if (inMenu) {
let badgeClass = "download-" + aValue;
gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, badgeClass);
}
}
}
return aValue;
},
_attention: DownloadsCommon.ATTENTION_NONE,
//////////////////////////////////////////////////////////////////////////////
//// User interface event functions
onWindowUnload() {
// This function is registered as an event listener, we can't use "this".
DownloadsIndicatorView.ensureTerminated();
},
onCommand(aEvent) {
// If the downloads button is in the menu panel, open the Library
let widgetGroup = CustomizableUI.getWidget("downloads-button");
if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
DownloadsPanel.showDownloadsHistory();
} else {
DownloadsPanel.showPanel();
}
aEvent.stopPropagation();
},
onDragOver(aEvent) {
browserDragAndDrop.dragOver(aEvent);
},
onDrop(aEvent) {
let dt = aEvent.dataTransfer;
// If dragged item is from our source, do not try to
// redownload already downloaded file.
if (dt.mozGetDataAt("application/x-moz-file", 0))
return;
let links = browserDragAndDrop.dropLinks(aEvent);
if (!links.length)
return;
let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document;
let handled = false;
for (let link of links) {
if (link.url.startsWith("about:"))
continue;
saveURL(link.url, link.name, null, true, true, null, sourceDoc);
handled = true;
}
if (handled) {
aEvent.preventDefault();
}
},
_indicator: null,
__indicatorCounter: null,
__indicatorProgress: null,
/**
* Returns a reference to the main indicator element, or null if the element
* is not present in the browser window yet.
*/
get indicator() {
if (this._indicator) {
return this._indicator;
}
let indicator = document.getElementById("downloads-button");
if (!indicator || indicator.getAttribute("indicator") != "true") {
return null;
}
return this._indicator = indicator;
},
get indicatorAnchor() {
let widget = CustomizableUI.getWidget("downloads-button")
.forWindow(window);
if (widget.overflowed) {
return widget.anchor;
}
return document.getElementById("downloads-indicator-anchor");
},
get _indicatorCounter() {
return this.__indicatorCounter ||
(this.__indicatorCounter = document.getElementById("downloads-indicator-counter"));
},
get _indicatorProgress() {
return this.__indicatorProgress ||
(this.__indicatorProgress = document.getElementById("downloads-indicator-progress"));
},
get notifier() {
return this._notifier ||
(this._notifier = document.getElementById("downloads-notification-anchor"));
},
_onCustomizedAway() {
this._indicator = null;
this.__indicatorCounter = null;
this.__indicatorProgress = null;
},
afterCustomize() {
// If the cached indicator is not the one currently in the document,
// invalidate our references
if (this._indicator != document.getElementById("downloads-button")) {
this._onCustomizedAway();
this._operational = false;
this.ensureTerminated();
this.ensureInitialized();
}
},
};
Object.defineProperty(this, "DownloadsIndicatorView", {
value: DownloadsIndicatorView,
enumerable: true,
writable: false
});

View file

@ -0,0 +1,36 @@
<?xml version="1.0"?>
<!-- -*- Mode: HTML; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- -->
<!-- vim: set ts=2 et sw=2 tw=80: -->
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
- You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE overlay [
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" >
%browserDTD;
]>
<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
id="indicatorOverlay">
<!-- We dynamically add the stack with the progress meter and notification icon,
originally loaded lazily because of performance reasons, to the existing
downloads-button. -->
<toolbarbutton id="downloads-button" indicator="true">
<!-- The panel's anchor area is smaller than the outer button, but must
always be visible and must not move or resize when the indicator
state changes, otherwise the panel could change its position or lose
its arrow unexpectedly. -->
<stack id="downloads-indicator-anchor"
consumeanchor="downloads-button">
<vbox id="downloads-indicator-progress-area" pack="center">
<description id="downloads-indicator-counter"/>
<progressmeter id="downloads-indicator-progress" class="plain"
min="0" max="100"/>
</vbox>
<vbox id="downloads-indicator-icon"/>
</stack>
</toolbarbutton>
</overlay>

View file

@ -0,0 +1,16 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
* content/browser/downloads/download.xml (content/download.xml)
content/browser/downloads/downloads.css (content/downloads.css)
content/browser/downloads/downloads.js (content/downloads.js)
* content/browser/downloads/downloadsOverlay.xul (content/downloadsOverlay.xul)
content/browser/downloads/indicator.js (content/indicator.js)
content/browser/downloads/indicatorOverlay.xul (content/indicatorOverlay.xul)
* content/browser/downloads/allDownloadsViewOverlay.xul (content/allDownloadsViewOverlay.xul)
content/browser/downloads/allDownloadsViewOverlay.js (content/allDownloadsViewOverlay.js)
* content/browser/downloads/contentAreaDownloadsView.xul (content/contentAreaDownloadsView.xul)
content/browser/downloads/contentAreaDownloadsView.js (content/contentAreaDownloadsView.js)
content/browser/downloads/contentAreaDownloadsView.css (content/contentAreaDownloadsView.css)

View file

@ -0,0 +1,22 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
with Files('*'):
BUG_COMPONENT = ('Firefox', 'Downloads Panel')
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
JAR_MANIFESTS += ['jar.mn']
EXTRA_JS_MODULES += [
'DownloadsCommon.jsm',
'DownloadsTaskbar.jsm',
'DownloadsViewUI.jsm',
]
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Downloads Panel')

View file

@ -0,0 +1,7 @@
"use strict";
module.exports = {
"extends": [
"../../../../../testing/mochitest/browser.eslintrc.js"
]
};

View file

@ -0,0 +1,15 @@
[DEFAULT]
support-files = head.js
[browser_basic_functionality.js]
[browser_first_download_panel.js]
skip-if = os == "linux" # Bug 949434
[browser_overflow_anchor.js]
skip-if = os == "linux" # Bug 952422
[browser_confirm_unblock_download.js]
[browser_iframe_gone_mid_download.js]
[browser_indicatorDrop.js]
[browser_libraryDrop.js]
[browser_downloads_panel_block.js]
[browser_downloads_panel_footer.js]
[browser_downloads_panel_height.js]

View file

@ -0,0 +1,56 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
registerCleanupFunction(function*() {
yield task_resetState();
});
/**
* Make sure the downloads panel can display items in the right order and
* contains the expected data.
*/
add_task(function* test_basic_functionality() {
// Display one of each download state.
const DownloadData = [
{ state: nsIDM.DOWNLOAD_NOTSTARTED },
{ state: nsIDM.DOWNLOAD_PAUSED },
{ state: nsIDM.DOWNLOAD_FINISHED },
{ state: nsIDM.DOWNLOAD_FAILED },
{ state: nsIDM.DOWNLOAD_CANCELED },
];
// Wait for focus first
yield promiseFocus();
// Ensure that state is reset in case previous tests didn't finish.
yield task_resetState();
// For testing purposes, show all the download items at once.
var originalCountLimit = DownloadsView.kItemCountLimit;
DownloadsView.kItemCountLimit = DownloadData.length;
registerCleanupFunction(function () {
DownloadsView.kItemCountLimit = originalCountLimit;
});
// Populate the downloads database with the data required by this test.
yield task_addDownloads(DownloadData);
// Open the user interface and wait for data to be fully loaded.
yield task_openPanel();
// Test item data and count. This also tests the ordering of the display.
let richlistbox = document.getElementById("downloadsListBox");
/* disabled for failing intermittently (bug 767828)
is(richlistbox.children.length, DownloadData.length,
"There is the correct number of richlistitems");
*/
let itemCount = richlistbox.children.length;
for (let i = 0; i < itemCount; i++) {
let element = richlistbox.children[itemCount - i - 1];
let download = DownloadsView.itemForElement(element).download;
is(DownloadsCommon.stateOfDownload(download), DownloadData[i].state,
"Download states match up");
}
});

View file

@ -0,0 +1,92 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests the dialog which allows the user to unblock a downloaded file.
registerCleanupFunction(() => {});
function* assertDialogResult({ args, buttonToClick, expectedResult }) {
promiseAlertDialogOpen(buttonToClick);
is(yield DownloadsCommon.confirmUnblockDownload(args), expectedResult);
}
/**
* Tests the "unblock" dialog, for each of the possible verdicts.
*/
add_task(function* test_unblock_dialog_unblock() {
for (let verdict of [Downloads.Error.BLOCK_VERDICT_MALWARE,
Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
Downloads.Error.BLOCK_VERDICT_UNCOMMON]) {
let args = { verdict, window, dialogType: "unblock" };
// Test both buttons.
yield assertDialogResult({
args,
buttonToClick: "accept",
expectedResult: "unblock",
});
yield assertDialogResult({
args,
buttonToClick: "cancel",
expectedResult: "cancel",
});
}
});
/**
* Tests the "chooseUnblock" dialog for potentially unwanted downloads.
*/
add_task(function* test_chooseUnblock_dialog() {
let args = {
verdict: Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
window,
dialogType: "chooseUnblock",
};
// Test each of the three buttons.
yield assertDialogResult({
args,
buttonToClick: "accept",
expectedResult: "unblock",
});
yield assertDialogResult({
args,
buttonToClick: "cancel",
expectedResult: "cancel",
});
yield assertDialogResult({
args,
buttonToClick: "extra1",
expectedResult: "confirmBlock",
});
});
/**
* Tests the "chooseOpen" dialog for uncommon downloads.
*/
add_task(function* test_chooseOpen_dialog() {
let args = {
verdict: Downloads.Error.BLOCK_VERDICT_UNCOMMON,
window,
dialogType: "chooseOpen",
};
// Test each of the three buttons.
yield assertDialogResult({
args,
buttonToClick: "accept",
expectedResult: "open",
});
yield assertDialogResult({
args,
buttonToClick: "cancel",
expectedResult: "cancel",
});
yield assertDialogResult({
args,
buttonToClick: "extra1",
expectedResult: "confirmBlock",
});
});

View file

@ -0,0 +1,183 @@
"use strict";
add_task(function* mainTest() {
yield task_resetState();
let verdicts = [
Downloads.Error.BLOCK_VERDICT_UNCOMMON,
Downloads.Error.BLOCK_VERDICT_MALWARE,
Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
];
yield task_addDownloads(verdicts.map(v => makeDownload(v)));
// Check that the richlistitem for each download is correct.
for (let i = 0; i < verdicts.length; i++) {
yield openPanel();
// The current item is always the first one in the listbox since each
// iteration of this loop removes the item at the end.
let item = DownloadsView.richListBox.firstChild;
// Open the panel and click the item to show the subview.
EventUtils.sendMouseEvent({ type: "click" }, item);
yield promiseSubviewShown(true);
// Items are listed in newest-to-oldest order, so e.g. the first item's
// verdict is the last element in the verdicts array.
Assert.ok(DownloadsBlockedSubview.subview.getAttribute("verdict"),
verdicts[verdicts.count - i - 1]);
// Click the sliver of the main view that's still showing on the left to go
// back to it.
EventUtils.synthesizeMouse(DownloadsPanel.panel, 10, 10, {}, window);
yield promiseSubviewShown(false);
// Show the subview again.
EventUtils.sendMouseEvent({ type: "click" }, item);
yield promiseSubviewShown(true);
// Click the Open button. The download should be unblocked and then opened,
// i.e., unblockAndOpenDownload() should be called on the item. The panel
// should also be closed as a result, so wait for that too.
let unblockOpenPromise = promiseUnblockAndOpenDownloadCalled(item);
let hidePromise = promisePanelHidden();
EventUtils.synthesizeMouse(DownloadsBlockedSubview.elements.openButton,
10, 10, {}, window);
yield unblockOpenPromise;
yield hidePromise;
window.focus();
yield SimpleTest.promiseFocus(window);
// Reopen the panel and show the subview again.
yield openPanel();
EventUtils.sendMouseEvent({ type: "click" }, item);
yield promiseSubviewShown(true);
// Click the Remove button. The panel should close and the item should be
// removed from it.
EventUtils.synthesizeMouse(DownloadsBlockedSubview.elements.deleteButton,
10, 10, {}, window);
yield promisePanelHidden();
yield openPanel();
Assert.ok(!item.parentNode);
DownloadsPanel.hidePanel();
yield promisePanelHidden();
}
yield task_resetState();
});
function* openPanel() {
// This function is insane but something intermittently causes the panel to be
// closed as soon as it's opening on Linux ASAN. Maybe it would also happen
// on other build machines if the test ran often enough. Not only is the
// panel closed, it's closed while it's opening, leaving DownloadsPanel._state
// such that when you try to open the panel again, it thinks it's already
// open, but it's not. The result is that the test times out.
//
// What this does is call DownloadsPanel.showPanel over and over again until
// the panel is really open. There are a few wrinkles:
//
// (1) When panel.state is "open", check four more times (for a total of five)
// before returning to make the panel stays open.
// (2) If the panel is not open, check the _state. It should be either
// kStateUninitialized or kStateHidden. If it's not, then the panel is in the
// process of opening -- or maybe it's stuck in that process -- so reset the
// _state to kStateHidden.
// (3) If the _state is not kStateUninitialized or kStateHidden, then it may
// actually be properly opening and not stuck at all. To avoid always closing
// the panel while it's properly opening, use an exponential backoff mechanism
// for retries.
//
// If all that fails, then the test will time out, but it would have timed out
// anyway.
yield promiseFocus();
yield new Promise(resolve => {
let verifyCount = 5;
let backoff = 0;
let iBackoff = 0;
let interval = setInterval(() => {
if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
if (verifyCount > 0) {
verifyCount--;
} else {
clearInterval(interval);
resolve();
}
} else {
if (iBackoff < backoff) {
// Keep backing off before trying again.
iBackoff++;
} else {
// Try (or retry) opening the panel.
verifyCount = 5;
backoff = Math.max(1, 2 * backoff);
iBackoff = 0;
if (DownloadsPanel._state != DownloadsPanel.kStateUninitialized) {
DownloadsPanel._state = DownloadsPanel.kStateHidden;
}
DownloadsPanel.showPanel();
}
}
}, 100);
});
}
function promisePanelHidden() {
return new Promise(resolve => {
if (!DownloadsPanel.panel || DownloadsPanel.panel.state == "closed") {
resolve();
return;
}
DownloadsPanel.panel.addEventListener("popuphidden", function onHidden() {
DownloadsPanel.panel.removeEventListener("popuphidden", onHidden);
setTimeout(resolve, 0);
});
});
}
function makeDownload(verdict) {
return {
state: nsIDM.DOWNLOAD_DIRTY,
hasBlockedData: true,
errorObj: {
result: Components.results.NS_ERROR_FAILURE,
message: "Download blocked.",
becauseBlocked: true,
becauseBlockedByReputationCheck: true,
reputationCheckVerdict: verdict,
},
};
}
function promiseSubviewShown(shown) {
// More terribleness, but I'm tired of fighting intermittent timeouts on try.
// Just poll for the subview and wait a second before resolving the promise.
return new Promise(resolve => {
let interval = setInterval(() => {
if (shown == DownloadsBlockedSubview.view.showingSubView &&
!DownloadsBlockedSubview.view._transitioning) {
clearInterval(interval);
setTimeout(resolve, 1000);
return;
}
}, 0);
});
}
function promiseUnblockAndOpenDownloadCalled(item) {
return new Promise(resolve => {
let realFn = item._shell.unblockAndOpenDownload;
item._shell.unblockAndOpenDownload = () => {
item._shell.unblockAndOpenDownload = realFn;
resolve();
// unblockAndOpenDownload returns a promise (that's resolved when the file
// is opened).
return Promise.resolve();
};
});
}

View file

@ -0,0 +1,95 @@
"use strict";
function *task_openDownloadsSubPanel() {
let downloadSubPanel = document.getElementById("downloadSubPanel");
let popupShownPromise = BrowserTestUtils.waitForEvent(downloadSubPanel, "popupshown");
let downloadsDropmarker = document.getElementById("downloadsFooterDropmarker");
EventUtils.synthesizeMouseAtCenter(downloadsDropmarker, {}, window);
yield popupShownPromise;
}
add_task(function* test_openDownloadsFolder() {
yield SpecialPowers.pushPrefEnv({"set": [["browser.download.showPanelDropmarker", true]]});
yield task_openPanel();
yield task_openDownloadsSubPanel();
yield new Promise(resolve => {
sinon.stub(DownloadsCommon, "showDirectory", file => {
resolve(Downloads.getPreferredDownloadsDirectory().then(downloadsPath => {
is(file.path, downloadsPath, "Check the download folder path.");
}));
});
let itemOpenDownloadsFolder =
document.getElementById("downloadsDropdownItemOpenDownloadsFolder");
EventUtils.synthesizeMouseAtCenter(itemOpenDownloadsFolder, {}, window);
});
yield task_resetState();
});
add_task(function* test_clearList() {
const kTestCases = [{
downloads: [
{ state: nsIDM.DOWNLOAD_NOTSTARTED },
{ state: nsIDM.DOWNLOAD_FINISHED },
{ state: nsIDM.DOWNLOAD_FAILED },
{ state: nsIDM.DOWNLOAD_CANCELED },
],
expectClearListShown: true,
expectedItemNumber: 0,
},{
downloads: [
{ state: nsIDM.DOWNLOAD_NOTSTARTED },
{ state: nsIDM.DOWNLOAD_FINISHED },
{ state: nsIDM.DOWNLOAD_FAILED },
{ state: nsIDM.DOWNLOAD_PAUSED },
{ state: nsIDM.DOWNLOAD_CANCELED },
],
expectClearListShown: true,
expectedItemNumber: 1,
},{
downloads: [
{ state: nsIDM.DOWNLOAD_PAUSED },
],
expectClearListShown: false,
expectedItemNumber: 1,
}];
for (let testCase of kTestCases) {
yield verify_clearList(testCase);
}
});
function *verify_clearList(testCase) {
let downloads = testCase.downloads;
yield task_addDownloads(downloads);
yield task_openPanel();
is(DownloadsView._downloads.length, downloads.length,
"Expect the number of download items");
yield task_openDownloadsSubPanel();
let itemClearList = document.getElementById("downloadsDropdownItemClearList");
let itemNumberPromise = BrowserTestUtils.waitForCondition(() => {
return DownloadsView._downloads.length === testCase.expectedItemNumber;
});
if (testCase.expectClearListShown) {
isnot("true", itemClearList.getAttribute("hidden"),
"Should show Clear Preview Panel button");
EventUtils.synthesizeMouseAtCenter(itemClearList, {}, window);
} else {
is("true", itemClearList.getAttribute("hidden"),
"Should not show Clear Preview Panel button");
}
yield itemNumberPromise;
is(DownloadsView._downloads.length, testCase.expectedItemNumber,
"Download items remained.");
yield task_resetState();
}

View file

@ -0,0 +1,29 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* This test exists because we use a <panelmultiview> element and it handles
* some of the height changes for us. We need to verify that the height is
* updated correctly if downloads are removed while the panel is hidden.
*/
add_task(function* test_height_reduced_after_removal() {
yield task_addDownloads([
{ state: nsIDM.DOWNLOAD_FINISHED },
]);
yield task_openPanel();
let panel = document.getElementById("downloadsPanel");
let heightBeforeRemoval = panel.getBoundingClientRect().height;
// We want to close the panel before we remove the download from the list.
DownloadsPanel.hidePanel();
yield task_resetState();
yield task_openPanel();
let heightAfterRemoval = panel.getBoundingClientRect().height;
Assert.greater(heightBeforeRemoval, heightAfterRemoval);
yield task_resetState();
});

View file

@ -0,0 +1,57 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure the downloads panel only opens automatically on the first
* download it notices. All subsequent downloads, even across sessions, should
* not open the panel automatically.
*/
add_task(function* test_first_download_panel() {
// Clear the download panel has shown preference first as this test is used to
// verify this preference's behaviour.
let oldPrefValue = Services.prefs.getBoolPref("browser.download.panel.shown");
Services.prefs.setBoolPref("browser.download.panel.shown", false);
registerCleanupFunction(function*() {
// Clean up when the test finishes.
yield task_resetState();
// Set the preference instead of clearing it afterwards to ensure the
// right value is used no matter what the default was. This ensures the
// panel doesn't appear and affect other tests.
Services.prefs.setBoolPref("browser.download.panel.shown", oldPrefValue);
});
// Ensure that state is reset in case previous tests didn't finish.
yield task_resetState();
// With this set to false, we should automatically open the panel the first
// time a download is started.
DownloadsCommon.getData(window).panelHasShownBefore = false;
let promise = promisePanelOpened();
DownloadsCommon.getData(window)._notifyDownloadEvent("start");
yield promise;
// If we got here, that means the panel opened.
DownloadsPanel.hidePanel();
ok(DownloadsCommon.getData(window).panelHasShownBefore,
"Should have recorded that the panel was opened on a download.")
// Next, make sure that if we start another download, we don't open the
// panel automatically.
let originalOnPopupShown = DownloadsPanel.onPopupShown;
DownloadsPanel.onPopupShown = function () {
originalOnPopupShown.apply(this, arguments);
ok(false, "Should not have opened the downloads panel.");
};
DownloadsCommon.getData(window)._notifyDownloadEvent("start");
// Wait 2 seconds to ensure that the panel does not open.
yield new Promise(resolve => setTimeout(resolve, 2000));
DownloadsPanel.onPopupShown = originalOnPopupShown;
});

View file

@ -0,0 +1,62 @@
const SAVE_PER_SITE_PREF = "browser.download.lastDir.savePerSite";
function test_deleted_iframe(perSitePref, windowOptions={}) {
return function*() {
Services.prefs.setBoolPref(SAVE_PER_SITE_PREF, perSitePref);
let {DownloadLastDir} = Cu.import("resource://gre/modules/DownloadLastDir.jsm", {});
let win = yield promiseOpenAndLoadWindow(windowOptions);
let tab = win.gBrowser.addTab();
yield promiseTabLoadEvent(tab, "about:mozilla");
let doc = tab.linkedBrowser.contentDocument;
let iframe = doc.createElement("iframe");
doc.body.appendChild(iframe);
ok(iframe.contentWindow, "iframe should have a window");
let gDownloadLastDir = new DownloadLastDir(iframe.contentWindow);
let cw = iframe.contentWindow;
let promiseIframeWindowGone = new Promise((resolve, reject) => {
Services.obs.addObserver(function obs(subject, topic) {
if (subject == cw) {
Services.obs.removeObserver(obs, topic);
resolve();
}
}, "dom-window-destroyed", false);
});
iframe.remove();
yield promiseIframeWindowGone;
cw = null;
ok(!iframe.contentWindow, "Managed to destroy iframe");
let someDir = "blah";
try {
someDir = yield new Promise((resolve, reject) => {
gDownloadLastDir.getFileAsync("http://www.mozilla.org/", function(dir) {
resolve(dir);
});
});
} catch (ex) {
ok(false, "Got an exception trying to get the directory where things should be saved.");
Cu.reportError(ex);
}
// NB: someDir can legitimately be null here when set, hence the 'blah' workaround:
isnot(someDir, "blah", "Should get a file even after the window was destroyed.");
try {
gDownloadLastDir.setFile("http://www.mozilla.org/", null);
} catch (ex) {
ok(false, "Got an exception trying to set the directory where things should be saved.");
Cu.reportError(ex);
}
yield promiseWindowClosed(win);
Services.prefs.clearUserPref(SAVE_PER_SITE_PREF);
};
}
add_task(test_deleted_iframe(false));
add_task(test_deleted_iframe(false));
add_task(test_deleted_iframe(true, {private: true}));
add_task(test_deleted_iframe(true, {private: true}));

View file

@ -0,0 +1,67 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
"resource://testing-common/httpd.js");
registerCleanupFunction(function*() {
yield task_resetState();
yield task_clearHistory();
});
add_task(function* test_indicatorDrop() {
let downloadButton = document.getElementById("downloads-button");
ok(downloadButton, "download button present");
let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
getService(Ci.mozIJSSubScriptLoader);
let EventUtils = {};
scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
function* task_drop(urls) {
let dragData = [[{type: "text/plain", data: urls.join("\n")}]];
let list = yield Downloads.getList(Downloads.ALL);
let added = new Set();
let succeeded = new Set();
yield new Promise(function(resolve) {
let view = {
onDownloadAdded: function(download) {
added.add(download.source.url);
},
onDownloadChanged: function(download) {
if (!added.has(download.source.url))
return;
if (!download.succeeded)
return;
succeeded.add(download.source.url);
if (succeeded.size == urls.length) {
list.removeView(view).then(resolve);
}
}
};
list.addView(view).then(function() {
EventUtils.synthesizeDrop(downloadButton, downloadButton, dragData, "link", window);
});
});
for (let url of urls) {
ok(added.has(url), url + " is added to download");
}
}
// Ensure that state is reset in case previous tests didn't finish.
yield task_resetState();
yield setDownloadDir();
startServer();
yield* task_drop([httpUrl("file1.txt")]);
yield* task_drop([httpUrl("file1.txt"),
httpUrl("file2.txt"),
httpUrl("file3.txt")]);
});

View file

@ -0,0 +1,72 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
"resource://testing-common/httpd.js");
registerCleanupFunction(function*() {
yield task_resetState();
yield task_clearHistory();
});
add_task(function* test_indicatorDrop() {
let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
getService(Ci.mozIJSSubScriptLoader);
let EventUtils = {};
scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
function task_drop(win, urls) {
let dragData = [[{type: "text/plain", data: urls.join("\n")}]];
let listBox = win.document.getElementById("downloadsRichListBox");
ok(listBox, "download list box present");
let list = yield Downloads.getList(Downloads.ALL);
let added = new Set();
let succeeded = new Set();
yield new Promise(function(resolve) {
let view = {
onDownloadAdded: function(download) {
added.add(download.source.url);
},
onDownloadChanged: function(download) {
if (!added.has(download.source.url))
return;
if (!download.succeeded)
return;
succeeded.add(download.source.url);
if (succeeded.size == urls.length) {
list.removeView(view).then(resolve);
}
}
};
list.addView(view).then(function() {
EventUtils.synthesizeDrop(listBox, listBox, dragData, "link", win);
});
});
for (let url of urls) {
ok(added.has(url), url + " is added to download");
}
}
// Ensure that state is reset in case previous tests didn't finish.
yield task_resetState();
setDownloadDir();
startServer();
let win = yield openLibrary("Downloads");
registerCleanupFunction(function() {
win.close();
});
yield* task_drop(win, [httpUrl("file1.txt")]);
yield* task_drop(win, [httpUrl("file1.txt"),
httpUrl("file2.txt"),
httpUrl("file3.txt")]);
});

View file

@ -0,0 +1,115 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
registerCleanupFunction(function*() {
// Clean up when the test finishes.
yield task_resetState();
});
/**
* Make sure the downloads button and indicator overflows into the nav-bar
* chevron properly, and then when those buttons are clicked in the overflow
* panel that the downloads panel anchors to the chevron.
*/
add_task(function* test_overflow_anchor() {
// Ensure that state is reset in case previous tests didn't finish.
yield task_resetState();
// Record the original width of the window so we can put it back when
// this test finishes.
let oldWidth = window.outerWidth;
// The downloads button should not be overflowed to begin with.
let button = CustomizableUI.getWidget("downloads-button")
.forWindow(window);
ok(!button.overflowed, "Downloads button should not be overflowed.");
// Hack - we lock the size of the default flex-y items in the nav-bar,
// namely, the URL and search inputs. That way we can resize the
// window without worrying about them flexing.
const kFlexyItems = ["urlbar-container", "search-container"];
registerCleanupFunction(() => unlockWidth(kFlexyItems));
lockWidth(kFlexyItems);
// Resize the window to half of its original size. That should
// be enough to overflow the downloads button.
window.resizeTo(oldWidth / 2, window.outerHeight);
yield waitForOverflowed(button, true);
let promise = promisePanelOpened();
button.node.doCommand();
yield promise;
let panel = DownloadsPanel.panel;
let chevron = document.getElementById("nav-bar-overflow-button");
is(panel.anchorNode, chevron, "Panel should be anchored to the chevron.");
DownloadsPanel.hidePanel();
// Unlock the widths on the flex-y items.
unlockWidth(kFlexyItems);
// Put the window back to its original dimensions.
window.resizeTo(oldWidth, window.outerHeight);
// The downloads button should eventually be un-overflowed.
yield waitForOverflowed(button, false);
// Now try opening the panel again.
promise = promisePanelOpened();
button.node.doCommand();
yield promise;
is(panel.anchorNode.id, "downloads-indicator-anchor");
DownloadsPanel.hidePanel();
});
/**
* For some node IDs, finds the nodes and sets their min-width's to their
* current width, preventing them from flex-shrinking.
*
* @param aItemIDs an array of item IDs to set min-width on.
*/
function lockWidth(aItemIDs) {
for (let itemID of aItemIDs) {
let item = document.getElementById(itemID);
let curWidth = item.getBoundingClientRect().width + "px";
item.style.minWidth = curWidth;
}
}
/**
* Clears the min-width's set on a set of IDs by lockWidth.
*
* @param aItemIDs an array of ItemIDs to remove min-width on.
*/
function unlockWidth(aItemIDs) {
for (let itemID of aItemIDs) {
let item = document.getElementById(itemID);
item.style.minWidth = "";
}
}
/**
* Waits for a node to enter or exit the overflowed state.
*
* @param aItem the node to wait for.
* @param aIsOverflowed if we're waiting for the item to be overflowed.
*/
function waitForOverflowed(aItem, aIsOverflowed) {
let deferOverflow = Promise.defer();
if (aItem.overflowed == aIsOverflowed) {
return deferOverflow.resolve();
}
let observer = new MutationObserver(function(aMutations) {
if (aItem.overflowed == aIsOverflowed) {
observer.disconnect();
deferOverflow.resolve();
}
});
observer.observe(aItem.node, {attributes: true});
return deferOverflow.promise;
}

View file

@ -0,0 +1,300 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Provides infrastructure for automated download components tests.
*/
////////////////////////////////////////////////////////////////////////////////
//// Globals
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
"resource:///modules/DownloadsCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
const nsIDM = Ci.nsIDownloadManager;
var gTestTargetFile = FileUtils.getFile("TmpD", ["dm-ui-test.file"]);
gTestTargetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
// Load mocking/stubbing library, sinon
// docs: http://sinonjs.org/docs/
Services.scriptloader.loadSubScript("resource://testing-common/sinon-1.16.1.js");
registerCleanupFunction(function () {
gTestTargetFile.remove(false);
delete window.sinon;
delete window.setImmediate;
delete window.clearImmediate;
});
////////////////////////////////////////////////////////////////////////////////
//// Asynchronous support subroutines
function promiseOpenAndLoadWindow(aOptions)
{
return new Promise((resolve, reject) => {
let win = OpenBrowserWindow(aOptions);
win.addEventListener("load", function onLoad() {
win.removeEventListener("load", onLoad);
resolve(win);
});
});
}
/**
* Waits for a load (or custom) event to finish in a given tab. If provided
* load an uri into the tab.
*
* @param tab
* The tab to load into.
* @param [optional] url
* The url to load, or the current url.
* @param [optional] event
* The load event type to wait for. Defaults to "load".
* @return {Promise} resolved when the event is handled.
* @resolves to the received event
* @rejects if a valid load event is not received within a meaningful interval
*/
function promiseTabLoadEvent(tab, url, eventType="load")
{
let deferred = Promise.defer();
info("Wait tab event: " + eventType);
function handle(event) {
if (event.originalTarget != tab.linkedBrowser.contentDocument ||
event.target.location.href == "about:blank" ||
(url && event.target.location.href != url)) {
info("Skipping spurious '" + eventType + "'' event" +
" for " + event.target.location.href);
return;
}
// Remove reference to tab from the cleanup function:
realCleanup = () => {};
tab.linkedBrowser.removeEventListener(eventType, handle, true);
info("Tab event received: " + eventType);
deferred.resolve(event);
}
// Juggle a bit to avoid leaks:
let realCleanup = () => tab.linkedBrowser.removeEventListener(eventType, handle, true);
registerCleanupFunction(() => realCleanup());
tab.linkedBrowser.addEventListener(eventType, handle, true, true);
if (url)
tab.linkedBrowser.loadURI(url);
return deferred.promise;
}
function promiseWindowClosed(win)
{
let promise = new Promise((resolve, reject) => {
Services.obs.addObserver(function obs(subject, topic) {
if (subject == win) {
Services.obs.removeObserver(obs, topic);
resolve();
}
}, "domwindowclosed", false);
});
win.close();
return promise;
}
function promiseFocus()
{
let deferred = Promise.defer();
waitForFocus(deferred.resolve);
return deferred.promise;
}
function promisePanelOpened()
{
let deferred = Promise.defer();
if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
return deferred.resolve();
}
// Hook to wait until the panel is shown.
let originalOnPopupShown = DownloadsPanel.onPopupShown;
DownloadsPanel.onPopupShown = function () {
DownloadsPanel.onPopupShown = originalOnPopupShown;
originalOnPopupShown.apply(this, arguments);
// Defer to the next tick of the event loop so that we don't continue
// processing during the DOM event handler itself.
setTimeout(deferred.resolve, 0);
};
return deferred.promise;
}
function* task_resetState()
{
// Remove all downloads.
let publicList = yield Downloads.getList(Downloads.PUBLIC);
let downloads = yield publicList.getAll();
for (let download of downloads) {
publicList.remove(download);
yield download.finalize(true);
}
DownloadsPanel.hidePanel();
yield promiseFocus();
}
function* task_addDownloads(aItems)
{
let startTimeMs = Date.now();
let publicList = yield Downloads.getList(Downloads.PUBLIC);
for (let item of aItems) {
let download = {
source: {
url: "http://www.example.com/test-download.txt",
},
target: {
path: gTestTargetFile.path,
},
succeeded: item.state == nsIDM.DOWNLOAD_FINISHED,
canceled: item.state == nsIDM.DOWNLOAD_CANCELED ||
item.state == nsIDM.DOWNLOAD_PAUSED,
error: item.state == nsIDM.DOWNLOAD_FAILED ? new Error("Failed.") : null,
hasPartialData: item.state == nsIDM.DOWNLOAD_PAUSED,
hasBlockedData: item.hasBlockedData || false,
startTime: new Date(startTimeMs++),
};
// `"errorObj" in download` must be false when there's no error.
if (item.errorObj) {
download.errorObj = item.errorObj;
}
yield publicList.add(yield Downloads.createDownload(download));
}
}
function* task_openPanel()
{
yield promiseFocus();
let promise = promisePanelOpened();
DownloadsPanel.showPanel();
yield promise;
}
function* setDownloadDir() {
let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
tmpDir.append("testsavedir");
if (!tmpDir.exists()) {
tmpDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
registerCleanupFunction(function () {
try {
tmpDir.remove(true);
} catch (e) {
// On Windows debug build this may fail.
}
});
}
yield new Promise(resolve => {
SpecialPowers.pushPrefEnv({"set": [
["browser.download.folderList", 2],
["browser.download.dir", tmpDir, Ci.nsIFile],
]}, resolve);
});
}
let gHttpServer = null;
function startServer() {
gHttpServer = new HttpServer();
gHttpServer.start(-1);
registerCleanupFunction(function*() {
yield new Promise(function(resolve) {
gHttpServer.stop(resolve);
});
});
gHttpServer.registerPathHandler("/file1.txt", (request, response) => {
response.setStatusLine(null, 200, "OK");
response.write("file1");
response.processAsync();
response.finish();
});
gHttpServer.registerPathHandler("/file2.txt", (request, response) => {
response.setStatusLine(null, 200, "OK");
response.write("file2");
response.processAsync();
response.finish();
});
gHttpServer.registerPathHandler("/file3.txt", (request, response) => {
response.setStatusLine(null, 200, "OK");
response.write("file3");
response.processAsync();
response.finish();
});
}
function httpUrl(aFileName) {
return "http://localhost:" + gHttpServer.identity.primaryPort + "/" +
aFileName;
}
function task_clearHistory() {
return new Promise(function(resolve) {
Services.obs.addObserver(function observeCH(aSubject, aTopic, aData) {
Services.obs.removeObserver(observeCH, PlacesUtils.TOPIC_EXPIRATION_FINISHED);
resolve();
}, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
PlacesUtils.history.clear();
});
}
function openLibrary(aLeftPaneRoot) {
let library = window.openDialog("chrome://browser/content/places/places.xul",
"", "chrome,toolbar=yes,dialog=no,resizable",
aLeftPaneRoot);
return new Promise(resolve => {
waitForFocus(resolve, library);
});
}
function promiseAlertDialogOpen(buttonAction) {
return new Promise(resolve => {
Services.ww.registerNotification(function onOpen(subj, topic, data) {
if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
// The test listens for the "load" event which guarantees that the alert
// class has already been added (it is added when "DOMContentLoaded" is
// fired).
subj.addEventListener("load", function onLoad() {
subj.removeEventListener("load", onLoad);
if (subj.document.documentURI ==
"chrome://global/content/commonDialog.xul") {
Services.ww.unregisterNotification(onOpen);
let dialog = subj.document.getElementById("commonDialog");
ok(dialog.classList.contains("alert-dialog"),
"The dialog element should contain an alert class.");
let doc = subj.document.documentElement;
doc.getButton(buttonAction).click();
resolve();
}
});
}
});
});
}

View file

@ -0,0 +1,7 @@
"use strict";
module.exports = {
"extends": [
"../../../../../testing/xpcshell/xpcshell.eslintrc.js"
]
};

View file

@ -0,0 +1,18 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Provides infrastructure for automated download components tests.
*/
////////////////////////////////////////////////////////////////////////////////
//// Globals
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;
Cu.import("resource:///modules/DownloadsCommon.jsm");

View file

@ -0,0 +1,37 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests for the functions located directly in the "DownloadsCommon" object.
*/
function testFormatTimeLeft(aSeconds, aExpectedValue, aExpectedUnitString)
{
let expected = "";
if (aExpectedValue) {
// Format the expected result based on the current language.
expected = DownloadsCommon.strings[aExpectedUnitString](aExpectedValue);
}
do_check_eq(DownloadsCommon.formatTimeLeft(aSeconds), expected);
}
function run_test()
{
testFormatTimeLeft( 0, "", "");
testFormatTimeLeft( 1, "1", "shortTimeLeftSeconds");
testFormatTimeLeft( 29, "29", "shortTimeLeftSeconds");
testFormatTimeLeft( 30, "30", "shortTimeLeftSeconds");
testFormatTimeLeft( 31, "1", "shortTimeLeftMinutes");
testFormatTimeLeft( 60, "1", "shortTimeLeftMinutes");
testFormatTimeLeft( 89, "1", "shortTimeLeftMinutes");
testFormatTimeLeft( 90, "2", "shortTimeLeftMinutes");
testFormatTimeLeft( 91, "2", "shortTimeLeftMinutes");
testFormatTimeLeft( 3600, "1", "shortTimeLeftHours");
testFormatTimeLeft( 86400, "24", "shortTimeLeftHours");
testFormatTimeLeft( 169200, "47", "shortTimeLeftHours");
testFormatTimeLeft( 172800, "2", "shortTimeLeftDays");
testFormatTimeLeft(8553600, "99", "shortTimeLeftDays");
testFormatTimeLeft(8640000, "99", "shortTimeLeftDays");
}

View file

@ -0,0 +1,7 @@
[DEFAULT]
head = head.js
tail =
firefox-appdir = browser
skip-if = toolkit == 'android'
[test_DownloadsCommon.js]