From 71a0d8546c4cf07619eb2f9c3ea215fc9f34c9b3 Mon Sep 17 00:00:00 2001
From: Matt Brubeck
Date: Thu, 15 Aug 2013 22:02:42 -0400
Subject: [PATCH 01/46] Bug 852236 - re-enable localization in browser/metro
[r=Pike]
---
browser/locales/l10n.ini | 1 +
1 file changed, 1 insertion(+)
diff --git a/browser/locales/l10n.ini b/browser/locales/l10n.ini
index 5056b5ddc468..3a01b9677c7f 100644
--- a/browser/locales/l10n.ini
+++ b/browser/locales/l10n.ini
@@ -8,6 +8,7 @@ all = browser/locales/all-locales
[compare]
dirs = browser
+ browser/metro
extensions/reporter
other-licenses/branding/firefox
browser/branding/official
From e5293725990ef63f6e9373c9790ea0fb9dc4b0ad Mon Sep 17 00:00:00 2001
From: "Brian R. Bondy"
Date: Thu, 15 Aug 2013 22:30:57 -0400
Subject: [PATCH 02/46] Bug 888363 - Fix default browser activation. r=jimm
Extra notes:
GetModulePath has no changes, it is just moved up so it can be used earlier.
GetDesktopBrowserPath was moved into the class because it needs to use a member there.
GetDesktopBrowserPath implementation has changed, so please review it. Also its comments header is new and explains some stuff.
IsTargetBrowser is no longer used because we instead use 2 other boolean values set on the SetTarget call.
---
.../CommandExecuteHandler.cpp | 155 +++++++++---------
1 file changed, 76 insertions(+), 79 deletions(-)
diff --git a/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp
index f940b3ca7a16..a786e589400a 100644
--- a/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp
+++ b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp
@@ -37,9 +37,33 @@ static const WCHAR* kFirefoxExe = L"firefox.exe";
static const WCHAR* kMetroFirefoxExe = L"firefox.exe";
static const WCHAR* kDefaultMetroBrowserIDPathKey = L"FirefoxURL";
-static bool GetDesktopBrowserPath(CStringW& aPathBuffer);
static bool GetDefaultBrowserPath(CStringW& aPathBuffer);
+/*
+ * Retrieve our module dir path.
+ *
+ * @aPathBuffer Buffer to fill
+ */
+static bool GetModulePath(CStringW& aPathBuffer)
+{
+ WCHAR buffer[MAX_PATH];
+ memset(buffer, 0, sizeof(buffer));
+
+ if (!GetModuleFileName(NULL, buffer, MAX_PATH)) {
+ Log(L"GetModuleFileName failed.");
+ return false;
+ }
+
+ WCHAR* slash = wcsrchr(buffer, '\\');
+ if (!slash)
+ return false;
+ *slash = '\0';
+
+ aPathBuffer = buffer;
+ return true;
+}
+
+
template void SafeRelease(T **ppT)
{
if (*ppT) {
@@ -68,6 +92,8 @@ public:
mShellItemArray(NULL),
mUnkSite(NULL),
mTargetIsFileSystemLink(false),
+ mTargetIsDefaultBrowser(false),
+ mTargetIsBrowser(false),
mIsDesktopRequest(true),
mRequestMet(false)
{
@@ -279,6 +305,37 @@ public:
return S_OK;
}
+ /*
+ * Retrieve the target path if it is the default browser
+ * or if not default, retreives the target path if it is a firefox browser
+ * or if the target is not firefox, relies on a hack to get the
+ * 'module dir path\firefox.exe'
+ * The reason why it's not good to rely on the CEH path is because there is
+ * no guarantee win8 will use the CEH at our expected path. It has an in
+ * memory cache even if the registry is updated for the CEH path.
+ *
+ * @aPathBuffer Buffer to fill
+ */
+ bool GetDesktopBrowserPath(CStringW& aPathBuffer)
+ {
+ // If the target was the default browser itself then return early. Otherwise
+ // rely on a hack to check CEH path and calculate it relative to it.
+
+ if (mTargetIsDefaultBrowser || mTargetIsBrowser) {
+ aPathBuffer = mTarget;
+ return true;
+ }
+
+ if (!GetModulePath(aPathBuffer))
+ return false;
+
+ // ceh.exe sits in dist/bin root with the desktop browser. Since this
+ // is a firefox only component, this hardcoded filename is ok.
+ aPathBuffer.Append(L"\\");
+ aPathBuffer.Append(kFirefoxExe);
+ return true;
+ }
+
bool IsDefaultBrowser()
{
IApplicationAssociationRegistration* pAAR;
@@ -318,12 +375,10 @@ public:
// activating the Metro browser will fail. So fallback to the desktop.
CStringW selfPath;
GetDesktopBrowserPath(selfPath);
- selfPath.MakeLower();
CStringW browserPath;
GetDefaultBrowserPath(browserPath);
- browserPath.MakeLower();
- return selfPath == browserPath;
+ return !selfPath.CompareNoCase(browserPath);
}
private:
~CExecuteCommandVerb()
@@ -334,7 +389,6 @@ private:
void LaunchDesktopBrowser();
bool SetTargetPath(IShellItem* aItem);
- bool IsTargetBrowser();
long mRef;
IShellItemArray *mShellItemArray;
@@ -343,52 +397,13 @@ private:
CStringW mTarget;
CStringW mParameters;
bool mTargetIsFileSystemLink;
+ bool mTargetIsDefaultBrowser;
+ bool mTargetIsBrowser;
DWORD mKeyState;
bool mIsDesktopRequest;
bool mRequestMet;
};
-/*
- * Retrieve our module dir path.
- *
- * @aPathBuffer Buffer to fill
- */
-static bool GetModulePath(CStringW& aPathBuffer)
-{
- WCHAR buffer[MAX_PATH];
- memset(buffer, 0, sizeof(buffer));
-
- if (!GetModuleFileName(NULL, buffer, MAX_PATH)) {
- Log(L"GetModuleFileName failed.");
- return false;
- }
-
- WCHAR* slash = wcsrchr(buffer, '\\');
- if (!slash)
- return false;
- *slash = '\0';
-
- aPathBuffer = buffer;
- return true;
-}
-
-/*
- * Retrieve 'module dir path\firefox.exe'
- *
- * @aPathBuffer Buffer to fill
- */
-static bool GetDesktopBrowserPath(CStringW& aPathBuffer)
-{
- if (!GetModulePath(aPathBuffer))
- return false;
-
- // ceh.exe sits in dist/bin root with the desktop browser. Since this
- // is a firefox only component, this hardcoded filename is ok.
- aPathBuffer.Append(L"\\");
- aPathBuffer.Append(kFirefoxExe);
- return true;
-}
-
/*
* Retrieve the current default browser's path.
*
@@ -445,36 +460,6 @@ static bool GetDefaultBrowserAppModelID(WCHAR* aIDBuffer,
return true;
}
-/*
- * Determines if the current target points directly to a particular
- * browser or to a file or url.
- */
-bool CExecuteCommandVerb::IsTargetBrowser()
-{
- if (!mTarget.GetLength() || !mTargetIsFileSystemLink)
- return false;
-
- CStringW modulePath;
- if (!GetModulePath(modulePath))
- return false;
-
- modulePath.MakeLower();
-
- CStringW tmpTarget = mTarget;
- tmpTarget.Replace(L"\"", L"");
- tmpTarget.MakeLower();
-
- CStringW checkPath;
-
- checkPath = modulePath;
- checkPath.Append(L"\\");
- checkPath.Append(kFirefoxExe);
- if (tmpTarget == checkPath) {
- return true;
- }
- return false;
-}
-
namespace {
const FORMATETC kPlainTextFormat =
{CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
@@ -545,6 +530,7 @@ bool CExecuteCommandVerb::SetTargetPath(IShellItem* aItem)
mTargetIsFileSystemLink = (components.nScheme == INTERNET_SCHEME_FILE);
mTarget = cstrText;
+
return true;
}
@@ -562,6 +548,15 @@ bool CExecuteCommandVerb::SetTargetPath(IShellItem* aItem)
}
mTarget = str;
CoTaskMemFree(str);
+
+ CStringW defaultPath;
+ GetDefaultBrowserPath(defaultPath);
+ mTargetIsDefaultBrowser = !mTarget.CompareNoCase(defaultPath);
+
+ size_t browserEXELen = wcslen(kFirefoxExe);
+ mTargetIsBrowser = mTarget.GetLength() >= browserEXELen &&
+ !mTarget.Right(browserEXELen).CompareNoCase(kFirefoxExe);
+
return true;
}
@@ -577,9 +572,11 @@ void CExecuteCommandVerb::LaunchDesktopBrowser()
}
// If a taskbar shortcut, link or local file is clicked, the target will
- // be the browser exe or file.
+ // be the browser exe or file. Don't pass in -url for the target if the
+ // target is known to be a browser. Otherwise, one instance of Firefox
+ // will try to open another instance.
CStringW params;
- if (!IsTargetBrowser() && !mTarget.IsEmpty()) {
+ if (!mTargetIsDefaultBrowser && !mTargetIsBrowser && !mTarget.IsEmpty()) {
// Fallback to the module path if it failed to get the default browser.
GetDefaultBrowserPath(browserPath);
params += "-url ";
@@ -670,7 +667,7 @@ IFACEMETHODIMP CExecuteCommandVerb::Execute()
Log(L"Metro Launch: verb:%s appid:%s params:%s", mVerb, appModelID, mTarget);
// shortcuts to the application
- if (IsTargetBrowser()) {
+ if (mTargetIsDefaultBrowser) {
hr = activateMgr->ActivateApplication(appModelID, L"", AO_NONE, &processID);
Log(L"ActivateApplication result %X", hr);
// files
From cbd2b18756f27f6be1cc8e9f278272373621928c Mon Sep 17 00:00:00 2001
From: Mark Hammond
Date: Fri, 16 Aug 2013 14:39:43 +1000
Subject: [PATCH 03/46] Bug 897880 - Thumbnail service must not overwrite
existing thumbnails if it gets an error response. r=adw
---
.../thumbnails/BackgroundPageThumbs.jsm | 3 +-
toolkit/components/thumbnails/PageThumbs.jsm | 64 +++++++-
.../components/thumbnails/PageThumbsWorker.js | 24 ++-
.../content/backgroundPageThumbsContent.js | 3 +
.../components/thumbnails/test/Makefile.in | 1 +
.../test/browser_thumbnails_update.js | 149 +++++++++++++++---
toolkit/components/thumbnails/test/head.js | 46 ++++--
.../thumbnails/test/thumbnails_update.sjs | 39 +++++
8 files changed, 289 insertions(+), 40 deletions(-)
create mode 100644 toolkit/components/thumbnails/test/thumbnails_update.sjs
diff --git a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
index 87e6a6082a90..c00a8b2b5ff2 100644
--- a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
+++ b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
@@ -354,7 +354,8 @@ Capture.prototype = {
callOnDones();
return;
}
- PageThumbs._store(this.url, data.finalURL, data.imageData).then(callOnDones);
+ PageThumbs._store(this.url, data.finalURL, data.imageData, data.wasErrorResponse)
+ .then(callOnDones);
},
};
diff --git a/toolkit/components/thumbnails/PageThumbs.jsm b/toolkit/components/thumbnails/PageThumbs.jsm
index 91b581a179f4..090eefb861ba 100644
--- a/toolkit/components/thumbnails/PageThumbs.jsm
+++ b/toolkit/components/thumbnails/PageThumbs.jsm
@@ -203,18 +203,24 @@ this.PageThumbs = {
* capture process will send a notification via the observer service on
* capture, so consumers should watch for such observations if they want to
* be notified of an updated thumbnail.
+ *
+ * @return {Promise} that's resolved on completion.
*/
captureIfStale: function PageThumbs_captureIfStale(aUrl) {
+ let deferredResult = Promise.defer();
let filePath = PageThumbsStorage.getFilePathForURL(aUrl);
- PageThumbsWorker.post("isFileRecent", [filePath, MAX_THUMBNAIL_AGE_SECS]
+ PageThumbsWorker.post(
+ "isFileRecent",
+ [filePath, MAX_THUMBNAIL_AGE_SECS]
).then(result => {
if (!result.ok) {
// Sadly there is currently a circular dependency between this module
// and BackgroundPageThumbs, so do the import locally.
let BPT = Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", {}).BackgroundPageThumbs;
- BPT.capture(aUrl);
+ BPT.capture(aUrl, {onDone: deferredResult.resolve});
}
});
+ return deferredResult.promise;
},
/**
@@ -315,12 +321,15 @@ this.PageThumbs = {
let channel = aBrowser.docShell.currentDocumentChannel;
let originalURL = channel.originalURI.spec;
+ // see if this was an error response.
+ let wasError = this._isChannelErrorResponse(channel);
+
TaskUtils.spawn((function task() {
let isSuccess = true;
try {
let blob = yield this.captureToBlob(aBrowser.contentWindow);
let buffer = yield TaskUtils.readBlob(blob);
- yield this._store(originalURL, url, buffer);
+ yield this._store(originalURL, url, buffer, wasError);
} catch (_) {
isSuccess = false;
}
@@ -338,10 +347,27 @@ this.PageThumbs = {
* @param aOriginalURL The URL with which the capture was initiated.
* @param aFinalURL The URL to which aOriginalURL ultimately resolved.
* @param aData An ArrayBuffer containing the image data.
+ * @param aWasErrorResponse A boolean indicating if the capture was for a
+ * response that returned an error code.
* @return {Promise}
*/
- _store: function PageThumbs__store(aOriginalURL, aFinalURL, aData) {
+ _store: function PageThumbs__store(aOriginalURL, aFinalURL, aData, aWasErrorResponse) {
return TaskUtils.spawn(function () {
+ // If we got an error response, we only save it if we don't have an
+ // existing thumbnail. If we *do* have an existing thumbnail we "touch"
+ // it so we consider the old version fresh.
+ if (aWasErrorResponse) {
+ let result = yield PageThumbsStorage.touchIfExists(aFinalURL);
+ let exists = result.ok;
+ if (exists) {
+ if (aFinalURL != aOriginalURL) {
+ yield PageThumbsStorage.touchIfExists(aOriginalURL);
+ }
+ return;
+ }
+ // was an error response, but no existing thumbnail - just store
+ // that error response as something is (arguably) better than nothing.
+ }
let telemetryStoreTime = new Date();
yield PageThumbsStorage.writeData(aFinalURL, aData);
Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
@@ -463,6 +489,25 @@ this.PageThumbs = {
return [this._thumbnailWidth, this._thumbnailHeight];
},
+ /**
+ * Given a channel, returns true if it should be considered an "error
+ * response", false otherwise.
+ */
+ _isChannelErrorResponse: function(channel) {
+ // No valid document channel sounds like an error to me!
+ if (!channel)
+ return true;
+ if (!(channel instanceof Ci.nsIHttpChannel))
+ // it might be FTP etc, so assume it's ok.
+ return false;
+ try {
+ return !channel.requestSucceeded;
+ } catch (_) {
+ // not being able to determine success is surely failure!
+ return true;
+ }
+ },
+
_prefEnabled: function PageThumbs_prefEnabled() {
try {
return !Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled");
@@ -570,6 +615,17 @@ this.PageThumbsStorage = {
return PageThumbsWorker.post("wipe", [this.path]);
},
+ /**
+ * If the file holding the thumbnail for the given URL exists, update the
+ * modification time of the file to now and return true, otherwise return
+ * false.
+ *
+ * @return {Promise}
+ */
+ touchIfExists: function Storage_touchIfExists(aURL) {
+ return PageThumbsWorker.post("touchIfExists", [this.getFilePathForURL(aURL)]);
+ },
+
_calculateMD5Hash: function Storage_calculateMD5Hash(aValue) {
let hash = gCryptoHash;
let value = gUnicodeConverter.convertToByteArray(aValue);
diff --git a/toolkit/components/thumbnails/PageThumbsWorker.js b/toolkit/components/thumbnails/PageThumbsWorker.js
index a6ea6320e7f4..eeef77af59f2 100644
--- a/toolkit/components/thumbnails/PageThumbsWorker.js
+++ b/toolkit/components/thumbnails/PageThumbsWorker.js
@@ -176,6 +176,28 @@ let Agent = {
} finally {
iterator.close();
}
- }
+ },
+
+ touchIfExists: function Agent_touchIfExists(path) {
+ // No OS.File way to update the modification date of the file (bug 905509)
+ // so we open it for reading and writing, read 1 byte from the start of
+ // the file then write that byte back out.
+ // (Sadly it's impossible to use nsIFile here as we have no access to
+ // |Components|)
+ if (!File.exists(path)) {
+ return false;
+ }
+ let file = OS.File.open(path, { read: true, write: true });
+ try {
+ file.setPosition(0); // docs aren't clear on initial position, so seek to 0.
+ let byte = file.read(1);
+ file.setPosition(0);
+ file.write(byte);
+ } finally {
+ file.close();
+ }
+ return true;
+ },
+
};
diff --git a/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js b/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
index c40b7316e7ed..ee3006f45f9f 100644
--- a/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
+++ b/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
@@ -52,6 +52,8 @@ const backgroundPageThumbsContent = {
PageThumbs._captureToCanvas(content, canvas);
let captureTime = new Date() - captureDate;
+ let channel = docShell.currentDocumentChannel;
+ let isErrorResponse = PageThumbs._isChannelErrorResponse(channel);
let finalURL = this._webNav.currentURI.spec;
let fileReader = Cc["@mozilla.org/files/filereader;1"].
createInstance(Ci.nsIDOMFileReader);
@@ -64,6 +66,7 @@ const backgroundPageThumbsContent = {
CAPTURE_PAGE_LOAD_TIME_MS: pageLoadTime,
CAPTURE_CANVAS_DRAW_TIME_MS: captureTime,
},
+ wasErrorResponse: isErrorResponse,
});
};
canvas.toBlob(blob => fileReader.readAsArrayBuffer(blob));
diff --git a/toolkit/components/thumbnails/test/Makefile.in b/toolkit/components/thumbnails/test/Makefile.in
index 4a38d68c548b..335c1dbe7f0d 100644
--- a/toolkit/components/thumbnails/test/Makefile.in
+++ b/toolkit/components/thumbnails/test/Makefile.in
@@ -28,6 +28,7 @@ MOCHITEST_BROWSER_FILES := \
background_red_redirect.sjs \
privacy_cache_control.sjs \
thumbnails_background.sjs \
+ thumbnails_update.sjs \
$(NULL)
include $(topsrcdir)/config/rules.mk
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_update.js b/toolkit/components/thumbnails/test/browser_thumbnails_update.js
index d7a4bdba34e3..d6f959b66b50 100644
--- a/toolkit/components/thumbnails/test/browser_thumbnails_update.js
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_update.js
@@ -5,20 +5,56 @@
* These tests check the auto-update facility of the thumbnail service.
*/
-let numNotifications = 0;
-const URL = "data:text/html;charset=utf-8,";
-
-function observe(subject, topic, data) {
- is(topic, "page-thumbnail:create", "got expected topic");
- is(data, URL, "data is our test URL");
- if (++numNotifications == 2) {
- // This is the final notification and signals test success...
- Services.obs.removeObserver(observe, "page-thumbnail:create");
- next();
+function runTests() {
+ // A "trampoline" - a generator that iterates over sub-iterators
+ let tests = [
+ simpleCaptureTest,
+ errorResponseUpdateTest,
+ goodResponseUpdateTest,
+ foregroundErrorResponseUpdateTest,
+ foregroundGoodResponseUpdateTest
+ ];
+ for (let test of tests) {
+ info("Running subtest " + test.name);
+ for (let iterator of test())
+ yield iterator;
}
}
-function runTests() {
+function ensureThumbnailStale(url) {
+ // We go behind the back of the thumbnail service and change the
+ // mtime of the file to be in the past.
+ let fname = PageThumbsStorage.getFilePathForURL(url);
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(fname);
+ ok(file.exists(), fname + " should exist");
+ // Set it as very stale...
+ file.lastModifiedTime = Date.now() - 1000000000;
+}
+
+function getThumbnailModifiedTime(url) {
+ let fname = PageThumbsStorage.getFilePathForURL(url);
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(fname);
+ return file.lastModifiedTime;
+}
+
+// The tests!
+/* Check functionality of a normal "captureIfStale" request */
+function simpleCaptureTest() {
+ let numNotifications = 0;
+ const URL = "data:text/html;charset=utf-8,";
+
+ function observe(subject, topic, data) {
+ is(topic, "page-thumbnail:create", "got expected topic");
+ is(data, URL, "data is our test URL");
+ if (++numNotifications == 2) {
+ // This is the final notification and signals test success...
+ Services.obs.removeObserver(observe, "page-thumbnail:create");
+ next();
+ }
+ }
+
Services.obs.addObserver(observe, "page-thumbnail:create", false);
// Create a tab with a red background.
yield addTab(URL);
@@ -26,6 +62,8 @@ function runTests() {
// Capture the screenshot.
PageThumbs.captureAndStore(browser, function () {
+ // done with the tab.
+ gBrowser.removeTab(gBrowser.selectedTab);
// We've got a capture so should have seen the observer.
is(numNotifications, 1, "got notification of item being created.");
// The capture is now "fresh" - so requesting the URL should not cause
@@ -33,18 +71,89 @@ function runTests() {
PageThumbs.captureIfStale(URL);
is(numNotifications, 1, "still only 1 notification of item being created.");
- // Now we will go behind the back of the thumbnail service and change the
- // mtime of the file to be in the past.
- let fname = PageThumbsStorage.getFilePathForURL(URL);
- let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.initWithPath(fname);
- ok(file.exists(), fname + " doesn't exist");
- // Set it as very stale...
- file.lastModifiedTime = Date.now() - 1000000000;
+ ensureThumbnailStale(URL);
// Ask for it to be updated.
PageThumbs.captureIfStale(URL);
// But it's async, so wait - our observer above will call next() when
// the notification comes.
});
- yield undefined; // wait for callbacks to call 'next'...
+ yield undefined // wait for callbacks to call 'next'...
+}
+
+/* Check functionality of a background capture when there is an error response
+ from the server.
+ */
+function errorResponseUpdateTest() {
+ const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?fail";
+ yield addTab(URL);
+
+ yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ // update the thumbnail to be stale, then re-request it. The server will
+ // return a 400 response and a red thumbnail.
+ // The b/g service should (a) not save the thumbnail and (b) update the file
+ // to have an mtime of "now" - so we (a) check the thumbnail remains green
+ // and (b) check the mtime of the file is >= now.
+ ensureThumbnailStale(URL);
+ let now = Date.now();
+ PageThumbs.captureIfStale(URL).then(() => {
+ ok(getThumbnailModifiedTime(URL) >= now, "modified time should be >= now");
+ retrieveImageDataForURL(URL, function ([r, g, b]) {
+ is("" + [r,g,b], "" + [0, 255, 0], "thumbnail is still green");
+ next();
+ });
+ }).then(null, err => {ok(false, "Error in captureIfStale: " + err)});
+ yield undefined; // wait for callback to call 'next'...
+}
+
+/* Check functionality of a background capture when there is a non-error
+ response from the server. This test is somewhat redundant - although it is
+ using a http:// URL instead of a data: url like most others.
+ */
+function goodResponseUpdateTest() {
+ const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?ok";
+ yield addTab(URL);
+ let browser = gBrowser.selectedBrowser;
+
+ yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
+ // update the thumbnail to be stale, then re-request it. The server will
+ // return a 200 response and a red thumbnail - so that new thumbnail should
+ // end up captured.
+ ensureThumbnailStale(URL);
+ let now = Date.now();
+ PageThumbs.captureIfStale(URL).then(() => {
+ ok(getThumbnailModifiedTime(URL) >= now, "modified time should be >= now");
+ // the captureIfStale request saw a 200 response with the red body, so we
+ // expect to see the red version here.
+ retrieveImageDataForURL(URL, function ([r, g, b]) {
+ is("" + [r,g,b], "" + [255, 0, 0], "thumbnail is now red");
+ next();
+ });
+ }).then(null, err => {ok(false, "Error in captureIfStale: " + err)});
+ yield undefined; // wait for callback to call 'next'...
+}
+
+/* Check functionality of a foreground capture when there is an error response
+ from the server.
+ */
+function foregroundErrorResponseUpdateTest() {
+ const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?fail";
+ yield addTab(URL);
+ yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ // do it again - the server will return a 400, so the foreground service
+ // should not update it.
+ yield addTab(URL);
+ yield captureAndCheckColor(0, 255, 0, "we still have a green thumbnail");
+}
+
+function foregroundGoodResponseUpdateTest() {
+ const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?ok";
+ yield addTab(URL);
+ yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ // do it again - the server will return a 200, so the foreground service
+ // should update it.
+ yield addTab(URL);
+ yield captureAndCheckColor(255, 0, 0, "we now have a red thumbnail");
}
diff --git a/toolkit/components/thumbnails/test/head.js b/toolkit/components/thumbnails/test/head.js
index 77047a38b051..61cf938adf38 100644
--- a/toolkit/components/thumbnails/test/head.js
+++ b/toolkit/components/thumbnails/test/head.js
@@ -52,8 +52,13 @@ let TestRunner = {
next: function () {
try {
let value = TestRunner._iter.next();
- if (value && typeof value.then == "function")
- value.then(next);
+ if (value && typeof value.then == "function") {
+ value.then(result => {
+ next(result);
+ }, error => {
+ ok(false, error + "\n" + error.stack);
+ });
+ }
} catch (e if e instanceof StopIteration) {
finish();
}
@@ -129,20 +134,33 @@ function captureAndCheckColor(aRed, aGreen, aBlue, aMessage) {
function retrieveImageDataForURL(aURL, aCallback) {
let width = 100, height = 100;
let thumb = PageThumbs.getThumbnailURL(aURL, width, height);
+ // create a tab with a chrome:// URL so it can host the thumbnail image.
+ // Note that we tried creating the element directly in the top-level chrome
+ // document, but this caused a strange problem:
+ // * call this with the url of an image.
+ // * immediately change the image content.
+ // * call this again with the same url (now holding different content)
+ // The original image data would be used. Maybe the img hadn't been
+ // collected yet and the platform noticed the same URL, so reused the
+ // content? Not sure - but this solves the problem.
+ addTab("chrome://global/content/mozilla.xhtml", () => {
+ let doc = gBrowser.selectedBrowser.contentDocument;
+ let htmlns = "http://www.w3.org/1999/xhtml";
+ let img = doc.createElementNS(htmlns, "img");
+ img.setAttribute("src", thumb);
- let htmlns = "http://www.w3.org/1999/xhtml";
- let img = document.createElementNS(htmlns, "img");
- img.setAttribute("src", thumb);
+ whenLoaded(img, function () {
+ let canvas = document.createElementNS(htmlns, "canvas");
+ canvas.setAttribute("width", width);
+ canvas.setAttribute("height", height);
- whenLoaded(img, function () {
- let canvas = document.createElementNS(htmlns, "canvas");
- canvas.setAttribute("width", width);
- canvas.setAttribute("height", height);
-
- // Draw the image to a canvas and compare the pixel color values.
- let ctx = canvas.getContext("2d");
- ctx.drawImage(img, 0, 0, width, height);
- aCallback(ctx.getImageData(0, 0, 100, 100).data);
+ // Draw the image to a canvas and compare the pixel color values.
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0, width, height);
+ let result = ctx.getImageData(0, 0, 100, 100).data;
+ gBrowser.removeTab(gBrowser.selectedTab);
+ aCallback(result);
+ });
});
}
diff --git a/toolkit/components/thumbnails/test/thumbnails_update.sjs b/toolkit/components/thumbnails/test/thumbnails_update.sjs
new file mode 100644
index 000000000000..8ae4d79c9810
--- /dev/null
+++ b/toolkit/components/thumbnails/test/thumbnails_update.sjs
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This server-side script primarily must return different *content* for the
+// second request than it did for the first.
+// Also, it should be able to return an error response when requested for the
+// second response.
+// So the basic tests will be to grab the thumbnail, then request it to be
+// grabbed again:
+// * If the second request succeeded, the new thumbnail should exist.
+// * If the second request is an error, the new thumbnail should be ignored.
+
+function handleRequest(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/html;charset=utf-8", false);
+ // we want to disable gBrowserThumbnails on-load capture for these responses,
+ // so set as a "no-store" response.
+ aResponse.setHeader("Cache-Control", "no-store");
+
+ let doneError = getState(aRequest.queryString);
+ if (!doneError) {
+ // first request - return a response with a green body and 200 response.
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK - It's green");
+ aResponse.write("");
+ // set the state so the next request does the "second request" thing below.
+ setState(aRequest.queryString, "yep");
+ } else {
+ // second request - this will be done by the b/g service.
+ // We always return a red background, but depending on the query string we
+ // return either a 200 or 401 response.
+ if (aRequest.queryString == "fail")
+ aResponse.setStatusLine(aRequest.httpVersion, 401, "Oh no you don't");
+ else
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK - It's red");
+ aResponse.write("");
+ // reset the error state incase this ends up being reused for the
+ // same url and querystring.
+ setState(aRequest.queryString, "");
+ }
+}
From b103cc259b71a260a44f79309cdc5269ae1f29d7 Mon Sep 17 00:00:00 2001
From: Tim Taubert
Date: Fri, 16 Aug 2013 09:58:04 +0200
Subject: [PATCH 04/46] Bug 904460 - Pass _firstTabs 'parameter' as an argument
to restoreWindow() rather than tacking it onto the state object; r=smacleod
---
.../sessionstore/src/SessionStore.jsm | 63 ++++++++++---------
1 file changed, 35 insertions(+), 28 deletions(-)
diff --git a/browser/components/sessionstore/src/SessionStore.jsm b/browser/components/sessionstore/src/SessionStore.jsm
index c1e2db644170..b7492dfb3539 100644
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -741,12 +741,11 @@ let SessionStoreInternal = {
Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
} else {
TelemetryTimestamps.add("sessionRestoreRestoring");
- // make sure that the restored tabs are first in the window
- aInitialState._firstTabs = true;
this._restoreCount = aInitialState.windows ? aInitialState.windows.length : 0;
let overwrite = this._isCmdLineEmpty(aWindow, aInitialState);
- this.restoreWindow(aWindow, aInitialState, overwrite);
+ let options = {firstWindow: true, overwriteTabs: overwrite};
+ this.restoreWindow(aWindow, aInitialState, options);
// _loadState changed from "stopped" to "running". Save the session's
// load state immediately so that crashes happening during startup
@@ -764,8 +763,9 @@ let SessionStoreInternal = {
}
// this window was opened by _openWindowWithState
else if (!this._isWindowLoaded(aWindow)) {
- let followUp = this._statesToRestore[aWindow.__SS_restoreID].windows.length == 1;
- this.restoreWindow(aWindow, this._statesToRestore[aWindow.__SS_restoreID], true, followUp);
+ let state = this._statesToRestore[aWindow.__SS_restoreID];
+ let options = {overwriteTabs: true, isFollowUp: state.windows.length == 1};
+ this.restoreWindow(aWindow, state, options);
}
// The user opened another, non-private window after starting up with
// a single private one. Let's restore the session we actually wanted to
@@ -773,10 +773,9 @@ let SessionStoreInternal = {
else if (this._deferredInitialState && !isPrivateWindow &&
aWindow.toolbar.visible) {
- this._deferredInitialState._firstTabs = true;
this._restoreCount = this._deferredInitialState.windows ?
this._deferredInitialState.windows.length : 0;
- this.restoreWindow(aWindow, this._deferredInitialState, false);
+ this.restoreWindow(aWindow, this._deferredInitialState, {firstWindow: true});
this._deferredInitialState = null;
}
else if (this._restoreLastWindow && aWindow.toolbar.visible &&
@@ -837,7 +836,8 @@ let SessionStoreInternal = {
// Ensure that the window state isn't hidden
this._restoreCount = 1;
let state = { windows: [newWindowState] };
- this.restoreWindow(aWindow, state, this._isCmdLineEmpty(aWindow, state));
+ let options = {overwriteTabs: this._isCmdLineEmpty(aWindow, state)};
+ this.restoreWindow(aWindow, state, options);
}
}
// we actually restored the session just now.
@@ -1399,7 +1399,7 @@ let SessionStoreInternal = {
this._restoreCount = state.windows ? state.windows.length : 0;
// restore to the given state
- this.restoreWindow(window, state, true);
+ this.restoreWindow(window, state, {overwriteTabs: true});
},
getWindowState: function ssi_getWindowState(aWindow) {
@@ -1419,7 +1419,7 @@ let SessionStoreInternal = {
if (!aWindow.__SSi)
throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
- this.restoreWindow(aWindow, aState, aOverwrite);
+ this.restoreWindow(aWindow, aState, {overwriteTabs: aOverwrite});
},
getTabState: function ssi_getTabState(aTab) {
@@ -1780,7 +1780,8 @@ let SessionStoreInternal = {
// in _preWindowToRestoreInto will prevent most (all?) Panorama
// weirdness but we will still merge other extData.
// Bug 588217 should make this go away by merging the group data.
- this.restoreWindow(windowToUse, { windows: [winState] }, canOverwriteTabs, true);
+ let options = {overwriteTabs: canOverwriteTabs, isFollowUp: true};
+ this.restoreWindow(windowToUse, { windows: [winState] }, options);
}
else {
this._openWindowWithState({ windows: [winState] });
@@ -2613,13 +2614,19 @@ let SessionStoreInternal = {
* Window reference
* @param aState
* JS object or its eval'able source
- * @param aOverwriteTabs
- * bool overwrite existing tabs w/ new ones
- * @param aFollowUp
- * bool this isn't the restoration of the first window
+ * @param aOptions
+ * {overwriteTabs: true} to overwrite existing tabs w/ new ones
+ * {isFollowUp: true} if this is not the restoration of the 1st window
+ * {firstWindow: true} if this is the first non-private window we're
+ * restoring in this session, that might open an
+ * external link as well
*/
- restoreWindow: function ssi_restoreWindow(aWindow, aState, aOverwriteTabs, aFollowUp) {
- if (!aFollowUp) {
+ restoreWindow: function ssi_restoreWindow(aWindow, aState, aOptions = {}) {
+ let overwriteTabs = aOptions && aOptions.overwriteTabs;
+ let isFollowUp = aOptions && aOptions.isFollowUp;
+ let firstWindow = aOptions && aOptions.firstWindow;
+
+ if (isFollowUp) {
this.windowToFocus = aWindow;
}
// initialize window if necessary
@@ -2670,13 +2677,13 @@ let SessionStoreInternal = {
}
// don't restore a single blank tab when we've had an external
// URL passed in for loading at startup (cf. bug 357419)
- else if (root._firstTabs && !aOverwriteTabs && winData.tabs.length == 1 &&
+ else if (firstWindow && !overwriteTabs && winData.tabs.length == 1 &&
(!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) {
winData.tabs = [];
}
var tabbrowser = aWindow.gBrowser;
- var openTabCount = aOverwriteTabs ? tabbrowser.browsers.length : -1;
+ var openTabCount = overwriteTabs ? tabbrowser.browsers.length : -1;
var newTabCount = winData.tabs.length;
var tabs = [];
@@ -2686,14 +2693,14 @@ let SessionStoreInternal = {
tabstrip.smoothScroll = false;
// unpin all tabs to ensure they are not reordered in the next loop
- if (aOverwriteTabs) {
+ if (overwriteTabs) {
for (let t = tabbrowser._numPinnedTabs - 1; t > -1; t--)
tabbrowser.unpinTab(tabbrowser.tabs[t]);
}
// make sure that the selected tab won't be closed in order to
// prevent unnecessary flickering
- if (aOverwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount)
+ if (overwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount)
tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1);
let numVisibleTabs = 0;
@@ -2703,7 +2710,7 @@ let SessionStoreInternal = {
tabbrowser.tabs[t] :
tabbrowser.addTab("about:blank", {skipAnimation: true}));
// when resuming at startup: add additionally requested pages to the end
- if (!aOverwriteTabs && root._firstTabs) {
+ if (!overwriteTabs && firstWindow) {
tabbrowser.moveTabTo(tabs[t], t);
}
@@ -2730,7 +2737,7 @@ let SessionStoreInternal = {
// tabs will be rebuilt and marked if they need to be restored after loading
// state (in restoreHistoryPrecursor).
// We also want to invalidate any cached information on the tab state.
- if (aOverwriteTabs) {
+ if (overwriteTabs) {
for (let i = 0; i < tabbrowser.tabs.length; i++) {
let tab = tabbrowser.tabs[i];
TabStateCache.delete(tab);
@@ -2746,7 +2753,7 @@ let SessionStoreInternal = {
// count in case there are still tabs restoring.
if (!aWindow.__SS_tabsToRestore)
aWindow.__SS_tabsToRestore = 0;
- if (aOverwriteTabs)
+ if (overwriteTabs)
aWindow.__SS_tabsToRestore = newTabCount;
else
aWindow.__SS_tabsToRestore += newTabCount;
@@ -2759,12 +2766,12 @@ let SessionStoreInternal = {
aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID;
// when overwriting tabs, remove all superflous ones
- if (aOverwriteTabs && newTabCount < openTabCount) {
+ if (overwriteTabs && newTabCount < openTabCount) {
Array.slice(tabbrowser.tabs, newTabCount, openTabCount)
.forEach(tabbrowser.removeTab, tabbrowser);
}
- if (aOverwriteTabs) {
+ if (overwriteTabs) {
this.restoreWindowFeatures(aWindow, winData);
delete this._windows[aWindow.__SSi].extData;
}
@@ -2779,12 +2786,12 @@ let SessionStoreInternal = {
this._windows[aWindow.__SSi].extData[key] = winData.extData[key];
}
}
- if (aOverwriteTabs || root._firstTabs) {
+ if (overwriteTabs || firstWindow) {
this._windows[aWindow.__SSi]._closedTabs = winData._closedTabs || [];
}
this.restoreHistoryPrecursor(aWindow, tabs, winData.tabs,
- (aOverwriteTabs ? (parseInt(winData.selected) || 1) : 0), 0, 0);
+ (overwriteTabs ? (parseInt(winData.selected) || 1) : 0), 0, 0);
if (aState.scratchpads) {
ScratchpadManager.restoreSession(aState.scratchpads);
From 474a467bc0eb216f6ea1b44f3d8ea2548bcab41d Mon Sep 17 00:00:00 2001
From: "Carsten \"Tomcat\" Book"
Date: Fri, 16 Aug 2013 10:13:43 +0200
Subject: [PATCH 05/46] Backed out changeset 67714091ce5b (bug 897880) for
suspecion of causing a orange mochitest failure
---
.../thumbnails/BackgroundPageThumbs.jsm | 3 +-
toolkit/components/thumbnails/PageThumbs.jsm | 64 +-------
.../components/thumbnails/PageThumbsWorker.js | 24 +--
.../content/backgroundPageThumbsContent.js | 3 -
.../components/thumbnails/test/Makefile.in | 1 -
.../test/browser_thumbnails_update.js | 153 +++---------------
toolkit/components/thumbnails/test/head.js | 46 ++----
.../thumbnails/test/thumbnails_update.sjs | 39 -----
8 files changed, 42 insertions(+), 291 deletions(-)
delete mode 100644 toolkit/components/thumbnails/test/thumbnails_update.sjs
diff --git a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
index c00a8b2b5ff2..87e6a6082a90 100644
--- a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
+++ b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
@@ -354,8 +354,7 @@ Capture.prototype = {
callOnDones();
return;
}
- PageThumbs._store(this.url, data.finalURL, data.imageData, data.wasErrorResponse)
- .then(callOnDones);
+ PageThumbs._store(this.url, data.finalURL, data.imageData).then(callOnDones);
},
};
diff --git a/toolkit/components/thumbnails/PageThumbs.jsm b/toolkit/components/thumbnails/PageThumbs.jsm
index 090eefb861ba..91b581a179f4 100644
--- a/toolkit/components/thumbnails/PageThumbs.jsm
+++ b/toolkit/components/thumbnails/PageThumbs.jsm
@@ -203,24 +203,18 @@ this.PageThumbs = {
* capture process will send a notification via the observer service on
* capture, so consumers should watch for such observations if they want to
* be notified of an updated thumbnail.
- *
- * @return {Promise} that's resolved on completion.
*/
captureIfStale: function PageThumbs_captureIfStale(aUrl) {
- let deferredResult = Promise.defer();
let filePath = PageThumbsStorage.getFilePathForURL(aUrl);
- PageThumbsWorker.post(
- "isFileRecent",
- [filePath, MAX_THUMBNAIL_AGE_SECS]
+ PageThumbsWorker.post("isFileRecent", [filePath, MAX_THUMBNAIL_AGE_SECS]
).then(result => {
if (!result.ok) {
// Sadly there is currently a circular dependency between this module
// and BackgroundPageThumbs, so do the import locally.
let BPT = Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", {}).BackgroundPageThumbs;
- BPT.capture(aUrl, {onDone: deferredResult.resolve});
+ BPT.capture(aUrl);
}
});
- return deferredResult.promise;
},
/**
@@ -321,15 +315,12 @@ this.PageThumbs = {
let channel = aBrowser.docShell.currentDocumentChannel;
let originalURL = channel.originalURI.spec;
- // see if this was an error response.
- let wasError = this._isChannelErrorResponse(channel);
-
TaskUtils.spawn((function task() {
let isSuccess = true;
try {
let blob = yield this.captureToBlob(aBrowser.contentWindow);
let buffer = yield TaskUtils.readBlob(blob);
- yield this._store(originalURL, url, buffer, wasError);
+ yield this._store(originalURL, url, buffer);
} catch (_) {
isSuccess = false;
}
@@ -347,27 +338,10 @@ this.PageThumbs = {
* @param aOriginalURL The URL with which the capture was initiated.
* @param aFinalURL The URL to which aOriginalURL ultimately resolved.
* @param aData An ArrayBuffer containing the image data.
- * @param aWasErrorResponse A boolean indicating if the capture was for a
- * response that returned an error code.
* @return {Promise}
*/
- _store: function PageThumbs__store(aOriginalURL, aFinalURL, aData, aWasErrorResponse) {
+ _store: function PageThumbs__store(aOriginalURL, aFinalURL, aData) {
return TaskUtils.spawn(function () {
- // If we got an error response, we only save it if we don't have an
- // existing thumbnail. If we *do* have an existing thumbnail we "touch"
- // it so we consider the old version fresh.
- if (aWasErrorResponse) {
- let result = yield PageThumbsStorage.touchIfExists(aFinalURL);
- let exists = result.ok;
- if (exists) {
- if (aFinalURL != aOriginalURL) {
- yield PageThumbsStorage.touchIfExists(aOriginalURL);
- }
- return;
- }
- // was an error response, but no existing thumbnail - just store
- // that error response as something is (arguably) better than nothing.
- }
let telemetryStoreTime = new Date();
yield PageThumbsStorage.writeData(aFinalURL, aData);
Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
@@ -489,25 +463,6 @@ this.PageThumbs = {
return [this._thumbnailWidth, this._thumbnailHeight];
},
- /**
- * Given a channel, returns true if it should be considered an "error
- * response", false otherwise.
- */
- _isChannelErrorResponse: function(channel) {
- // No valid document channel sounds like an error to me!
- if (!channel)
- return true;
- if (!(channel instanceof Ci.nsIHttpChannel))
- // it might be FTP etc, so assume it's ok.
- return false;
- try {
- return !channel.requestSucceeded;
- } catch (_) {
- // not being able to determine success is surely failure!
- return true;
- }
- },
-
_prefEnabled: function PageThumbs_prefEnabled() {
try {
return !Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled");
@@ -615,17 +570,6 @@ this.PageThumbsStorage = {
return PageThumbsWorker.post("wipe", [this.path]);
},
- /**
- * If the file holding the thumbnail for the given URL exists, update the
- * modification time of the file to now and return true, otherwise return
- * false.
- *
- * @return {Promise}
- */
- touchIfExists: function Storage_touchIfExists(aURL) {
- return PageThumbsWorker.post("touchIfExists", [this.getFilePathForURL(aURL)]);
- },
-
_calculateMD5Hash: function Storage_calculateMD5Hash(aValue) {
let hash = gCryptoHash;
let value = gUnicodeConverter.convertToByteArray(aValue);
diff --git a/toolkit/components/thumbnails/PageThumbsWorker.js b/toolkit/components/thumbnails/PageThumbsWorker.js
index eeef77af59f2..a6ea6320e7f4 100644
--- a/toolkit/components/thumbnails/PageThumbsWorker.js
+++ b/toolkit/components/thumbnails/PageThumbsWorker.js
@@ -176,28 +176,6 @@ let Agent = {
} finally {
iterator.close();
}
- },
-
- touchIfExists: function Agent_touchIfExists(path) {
- // No OS.File way to update the modification date of the file (bug 905509)
- // so we open it for reading and writing, read 1 byte from the start of
- // the file then write that byte back out.
- // (Sadly it's impossible to use nsIFile here as we have no access to
- // |Components|)
- if (!File.exists(path)) {
- return false;
- }
- let file = OS.File.open(path, { read: true, write: true });
- try {
- file.setPosition(0); // docs aren't clear on initial position, so seek to 0.
- let byte = file.read(1);
- file.setPosition(0);
- file.write(byte);
- } finally {
- file.close();
- }
- return true;
- },
-
+ }
};
diff --git a/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js b/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
index ee3006f45f9f..c40b7316e7ed 100644
--- a/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
+++ b/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
@@ -52,8 +52,6 @@ const backgroundPageThumbsContent = {
PageThumbs._captureToCanvas(content, canvas);
let captureTime = new Date() - captureDate;
- let channel = docShell.currentDocumentChannel;
- let isErrorResponse = PageThumbs._isChannelErrorResponse(channel);
let finalURL = this._webNav.currentURI.spec;
let fileReader = Cc["@mozilla.org/files/filereader;1"].
createInstance(Ci.nsIDOMFileReader);
@@ -66,7 +64,6 @@ const backgroundPageThumbsContent = {
CAPTURE_PAGE_LOAD_TIME_MS: pageLoadTime,
CAPTURE_CANVAS_DRAW_TIME_MS: captureTime,
},
- wasErrorResponse: isErrorResponse,
});
};
canvas.toBlob(blob => fileReader.readAsArrayBuffer(blob));
diff --git a/toolkit/components/thumbnails/test/Makefile.in b/toolkit/components/thumbnails/test/Makefile.in
index 335c1dbe7f0d..4a38d68c548b 100644
--- a/toolkit/components/thumbnails/test/Makefile.in
+++ b/toolkit/components/thumbnails/test/Makefile.in
@@ -28,7 +28,6 @@ MOCHITEST_BROWSER_FILES := \
background_red_redirect.sjs \
privacy_cache_control.sjs \
thumbnails_background.sjs \
- thumbnails_update.sjs \
$(NULL)
include $(topsrcdir)/config/rules.mk
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_update.js b/toolkit/components/thumbnails/test/browser_thumbnails_update.js
index d6f959b66b50..d7a4bdba34e3 100644
--- a/toolkit/components/thumbnails/test/browser_thumbnails_update.js
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_update.js
@@ -5,56 +5,20 @@
* These tests check the auto-update facility of the thumbnail service.
*/
+let numNotifications = 0;
+const URL = "data:text/html;charset=utf-8,";
+
+function observe(subject, topic, data) {
+ is(topic, "page-thumbnail:create", "got expected topic");
+ is(data, URL, "data is our test URL");
+ if (++numNotifications == 2) {
+ // This is the final notification and signals test success...
+ Services.obs.removeObserver(observe, "page-thumbnail:create");
+ next();
+ }
+}
+
function runTests() {
- // A "trampoline" - a generator that iterates over sub-iterators
- let tests = [
- simpleCaptureTest,
- errorResponseUpdateTest,
- goodResponseUpdateTest,
- foregroundErrorResponseUpdateTest,
- foregroundGoodResponseUpdateTest
- ];
- for (let test of tests) {
- info("Running subtest " + test.name);
- for (let iterator of test())
- yield iterator;
- }
-}
-
-function ensureThumbnailStale(url) {
- // We go behind the back of the thumbnail service and change the
- // mtime of the file to be in the past.
- let fname = PageThumbsStorage.getFilePathForURL(url);
- let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.initWithPath(fname);
- ok(file.exists(), fname + " should exist");
- // Set it as very stale...
- file.lastModifiedTime = Date.now() - 1000000000;
-}
-
-function getThumbnailModifiedTime(url) {
- let fname = PageThumbsStorage.getFilePathForURL(url);
- let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.initWithPath(fname);
- return file.lastModifiedTime;
-}
-
-// The tests!
-/* Check functionality of a normal "captureIfStale" request */
-function simpleCaptureTest() {
- let numNotifications = 0;
- const URL = "data:text/html;charset=utf-8,";
-
- function observe(subject, topic, data) {
- is(topic, "page-thumbnail:create", "got expected topic");
- is(data, URL, "data is our test URL");
- if (++numNotifications == 2) {
- // This is the final notification and signals test success...
- Services.obs.removeObserver(observe, "page-thumbnail:create");
- next();
- }
- }
-
Services.obs.addObserver(observe, "page-thumbnail:create", false);
// Create a tab with a red background.
yield addTab(URL);
@@ -62,8 +26,6 @@ function simpleCaptureTest() {
// Capture the screenshot.
PageThumbs.captureAndStore(browser, function () {
- // done with the tab.
- gBrowser.removeTab(gBrowser.selectedTab);
// We've got a capture so should have seen the observer.
is(numNotifications, 1, "got notification of item being created.");
// The capture is now "fresh" - so requesting the URL should not cause
@@ -71,89 +33,18 @@ function simpleCaptureTest() {
PageThumbs.captureIfStale(URL);
is(numNotifications, 1, "still only 1 notification of item being created.");
- ensureThumbnailStale(URL);
+ // Now we will go behind the back of the thumbnail service and change the
+ // mtime of the file to be in the past.
+ let fname = PageThumbsStorage.getFilePathForURL(URL);
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(fname);
+ ok(file.exists(), fname + " doesn't exist");
+ // Set it as very stale...
+ file.lastModifiedTime = Date.now() - 1000000000;
// Ask for it to be updated.
PageThumbs.captureIfStale(URL);
// But it's async, so wait - our observer above will call next() when
// the notification comes.
});
- yield undefined // wait for callbacks to call 'next'...
-}
-
-/* Check functionality of a background capture when there is an error response
- from the server.
- */
-function errorResponseUpdateTest() {
- const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?fail";
- yield addTab(URL);
-
- yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
- gBrowser.removeTab(gBrowser.selectedTab);
- // update the thumbnail to be stale, then re-request it. The server will
- // return a 400 response and a red thumbnail.
- // The b/g service should (a) not save the thumbnail and (b) update the file
- // to have an mtime of "now" - so we (a) check the thumbnail remains green
- // and (b) check the mtime of the file is >= now.
- ensureThumbnailStale(URL);
- let now = Date.now();
- PageThumbs.captureIfStale(URL).then(() => {
- ok(getThumbnailModifiedTime(URL) >= now, "modified time should be >= now");
- retrieveImageDataForURL(URL, function ([r, g, b]) {
- is("" + [r,g,b], "" + [0, 255, 0], "thumbnail is still green");
- next();
- });
- }).then(null, err => {ok(false, "Error in captureIfStale: " + err)});
- yield undefined; // wait for callback to call 'next'...
-}
-
-/* Check functionality of a background capture when there is a non-error
- response from the server. This test is somewhat redundant - although it is
- using a http:// URL instead of a data: url like most others.
- */
-function goodResponseUpdateTest() {
- const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?ok";
- yield addTab(URL);
- let browser = gBrowser.selectedBrowser;
-
- yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
- // update the thumbnail to be stale, then re-request it. The server will
- // return a 200 response and a red thumbnail - so that new thumbnail should
- // end up captured.
- ensureThumbnailStale(URL);
- let now = Date.now();
- PageThumbs.captureIfStale(URL).then(() => {
- ok(getThumbnailModifiedTime(URL) >= now, "modified time should be >= now");
- // the captureIfStale request saw a 200 response with the red body, so we
- // expect to see the red version here.
- retrieveImageDataForURL(URL, function ([r, g, b]) {
- is("" + [r,g,b], "" + [255, 0, 0], "thumbnail is now red");
- next();
- });
- }).then(null, err => {ok(false, "Error in captureIfStale: " + err)});
- yield undefined; // wait for callback to call 'next'...
-}
-
-/* Check functionality of a foreground capture when there is an error response
- from the server.
- */
-function foregroundErrorResponseUpdateTest() {
- const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?fail";
- yield addTab(URL);
- yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
- gBrowser.removeTab(gBrowser.selectedTab);
- // do it again - the server will return a 400, so the foreground service
- // should not update it.
- yield addTab(URL);
- yield captureAndCheckColor(0, 255, 0, "we still have a green thumbnail");
-}
-
-function foregroundGoodResponseUpdateTest() {
- const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?ok";
- yield addTab(URL);
- yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
- gBrowser.removeTab(gBrowser.selectedTab);
- // do it again - the server will return a 200, so the foreground service
- // should update it.
- yield addTab(URL);
- yield captureAndCheckColor(255, 0, 0, "we now have a red thumbnail");
+ yield undefined; // wait for callbacks to call 'next'...
}
diff --git a/toolkit/components/thumbnails/test/head.js b/toolkit/components/thumbnails/test/head.js
index 61cf938adf38..77047a38b051 100644
--- a/toolkit/components/thumbnails/test/head.js
+++ b/toolkit/components/thumbnails/test/head.js
@@ -52,13 +52,8 @@ let TestRunner = {
next: function () {
try {
let value = TestRunner._iter.next();
- if (value && typeof value.then == "function") {
- value.then(result => {
- next(result);
- }, error => {
- ok(false, error + "\n" + error.stack);
- });
- }
+ if (value && typeof value.then == "function")
+ value.then(next);
} catch (e if e instanceof StopIteration) {
finish();
}
@@ -134,33 +129,20 @@ function captureAndCheckColor(aRed, aGreen, aBlue, aMessage) {
function retrieveImageDataForURL(aURL, aCallback) {
let width = 100, height = 100;
let thumb = PageThumbs.getThumbnailURL(aURL, width, height);
- // create a tab with a chrome:// URL so it can host the thumbnail image.
- // Note that we tried creating the element directly in the top-level chrome
- // document, but this caused a strange problem:
- // * call this with the url of an image.
- // * immediately change the image content.
- // * call this again with the same url (now holding different content)
- // The original image data would be used. Maybe the img hadn't been
- // collected yet and the platform noticed the same URL, so reused the
- // content? Not sure - but this solves the problem.
- addTab("chrome://global/content/mozilla.xhtml", () => {
- let doc = gBrowser.selectedBrowser.contentDocument;
- let htmlns = "http://www.w3.org/1999/xhtml";
- let img = doc.createElementNS(htmlns, "img");
- img.setAttribute("src", thumb);
- whenLoaded(img, function () {
- let canvas = document.createElementNS(htmlns, "canvas");
- canvas.setAttribute("width", width);
- canvas.setAttribute("height", height);
+ let htmlns = "http://www.w3.org/1999/xhtml";
+ let img = document.createElementNS(htmlns, "img");
+ img.setAttribute("src", thumb);
- // Draw the image to a canvas and compare the pixel color values.
- let ctx = canvas.getContext("2d");
- ctx.drawImage(img, 0, 0, width, height);
- let result = ctx.getImageData(0, 0, 100, 100).data;
- gBrowser.removeTab(gBrowser.selectedTab);
- aCallback(result);
- });
+ whenLoaded(img, function () {
+ let canvas = document.createElementNS(htmlns, "canvas");
+ canvas.setAttribute("width", width);
+ canvas.setAttribute("height", height);
+
+ // Draw the image to a canvas and compare the pixel color values.
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0, width, height);
+ aCallback(ctx.getImageData(0, 0, 100, 100).data);
});
}
diff --git a/toolkit/components/thumbnails/test/thumbnails_update.sjs b/toolkit/components/thumbnails/test/thumbnails_update.sjs
deleted file mode 100644
index 8ae4d79c9810..000000000000
--- a/toolkit/components/thumbnails/test/thumbnails_update.sjs
+++ /dev/null
@@ -1,39 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// This server-side script primarily must return different *content* for the
-// second request than it did for the first.
-// Also, it should be able to return an error response when requested for the
-// second response.
-// So the basic tests will be to grab the thumbnail, then request it to be
-// grabbed again:
-// * If the second request succeeded, the new thumbnail should exist.
-// * If the second request is an error, the new thumbnail should be ignored.
-
-function handleRequest(aRequest, aResponse) {
- aResponse.setHeader("Content-Type", "text/html;charset=utf-8", false);
- // we want to disable gBrowserThumbnails on-load capture for these responses,
- // so set as a "no-store" response.
- aResponse.setHeader("Cache-Control", "no-store");
-
- let doneError = getState(aRequest.queryString);
- if (!doneError) {
- // first request - return a response with a green body and 200 response.
- aResponse.setStatusLine(aRequest.httpVersion, 200, "OK - It's green");
- aResponse.write("");
- // set the state so the next request does the "second request" thing below.
- setState(aRequest.queryString, "yep");
- } else {
- // second request - this will be done by the b/g service.
- // We always return a red background, but depending on the query string we
- // return either a 200 or 401 response.
- if (aRequest.queryString == "fail")
- aResponse.setStatusLine(aRequest.httpVersion, 401, "Oh no you don't");
- else
- aResponse.setStatusLine(aRequest.httpVersion, 200, "OK - It's red");
- aResponse.write("");
- // reset the error state incase this ends up being reused for the
- // same url and querystring.
- setState(aRequest.queryString, "");
- }
-}
From 9d8c642be5930e21cb1f34cbbf24a965ad30050b Mon Sep 17 00:00:00 2001
From: Paolo Amadini
Date: Fri, 16 Aug 2013 11:02:15 +0200
Subject: [PATCH 06/46] Bug 899124 - Compute download speed and throttle
notifications. r=enn
---
.../downloads/src/DownloadsCommon.jsm | 4 +-
.../jsdownloads/src/DownloadCore.jsm | 80 +++++++++++++++++--
2 files changed, 77 insertions(+), 7 deletions(-)
diff --git a/browser/components/downloads/src/DownloadsCommon.jsm b/browser/components/downloads/src/DownloadsCommon.jsm
index 992317704dd4..e9722019a9ae 100644
--- a/browser/components/downloads/src/DownloadsCommon.jsm
+++ b/browser/components/downloads/src/DownloadsCommon.jsm
@@ -718,7 +718,7 @@ DownloadsDataCtor.prototype = {
return;
}
- this._downloadToDataItemMap.remove(aDownload);
+ this._downloadToDataItemMap.delete(aDownload);
this.dataItems[dataItem.downloadGuid] = null;
for (let view of this._views) {
view.onDataItemRemoved(dataItem);
@@ -1344,7 +1344,7 @@ DownloadsDataItem.prototype = {
this.currBytes = this._download.currentBytes;
this.maxBytes = this._download.totalBytes;
this.resumable = this._download.hasPartialData;
- this.speed = 0;
+ this.speed = this._download.speed;
this.percentComplete = this._download.progress;
},
diff --git a/toolkit/components/jsdownloads/src/DownloadCore.jsm b/toolkit/components/jsdownloads/src/DownloadCore.jsm
index cb34f66ff1d9..31007b784042 100644
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -80,6 +80,13 @@ function isString(aValue) {
(typeof aValue == "object" && "charAt" in aValue);
}
+/**
+ * This determines the minimum time interval between updates to the number of
+ * bytes transferred, and is a limiting factor to the sequence of readings used
+ * in calculating the speed of the download.
+ */
+const kProgressUpdateIntervalMs = 400;
+
////////////////////////////////////////////////////////////////////////////////
//// Download
@@ -183,6 +190,13 @@ Download.prototype = {
*/
currentBytes: 0,
+ /**
+ * Fractional number representing the speed of the download, in bytes per
+ * second. This value is zero when the download is stopped, and may be
+ * updated regardless of the value of hasProgress.
+ */
+ speed: 0,
+
/**
* Indicates whether, at this time, there is any partially downloaded data
* that can be used when restarting a failed or canceled download.
@@ -302,6 +316,9 @@ Download.prototype = {
let currentAttempt = deferAttempt.promise;
this._currentAttempt = currentAttempt;
+ // Restart the progress and speed calculations from scratch.
+ this._lastProgressTimeMs = 0;
+
// This function propagates progress from the DownloadSaver object, unless
// it comes in late from a download attempt that was replaced by a new one.
function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData)
@@ -387,6 +404,7 @@ Download.prototype = {
if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
this._currentAttempt = null;
this.stopped = true;
+ this.speed = 0;
this._notifyChange();
if (this.succeeded) {
this._deferSucceeded.resolve();
@@ -646,9 +664,20 @@ Download.prototype = {
}
},
+ /**
+ * Indicates the time of the last progress notification, expressed as the
+ * number of milliseconds since January 1, 1970, 00:00:00 UTC. This is zero
+ * until some bytes have actually been transferred.
+ */
+ _lastProgressTimeMs: 0,
+
/**
* Updates progress notifications based on the number of bytes transferred.
*
+ * The number of bytes transferred is not updated unless enough time passed
+ * since this function was last called. This limits the computation load, in
+ * particular when the listeners update the user interface in response.
+ *
* @param aCurrentBytes
* Number of bytes transferred until now.
* @param aTotalBytes
@@ -658,16 +687,57 @@ Download.prototype = {
* restarting the download if it fails or is canceled.
*/
_setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) {
- this.currentBytes = aCurrentBytes;
+ let changeMade = (this.hasPartialData != aHasPartialData);
this.hasPartialData = aHasPartialData;
- if (aTotalBytes != -1) {
+
+ // Unless aTotalBytes is -1, we can report partial download progress. In
+ // this case, notify when the related properties changed since last time.
+ if (aTotalBytes != -1 && (!this.hasProgress ||
+ this.totalBytes != aTotalBytes)) {
this.hasProgress = true;
this.totalBytes = aTotalBytes;
- if (aTotalBytes > 0) {
- this.progress = Math.floor(aCurrentBytes / aTotalBytes * 100);
+ changeMade = true;
+ }
+
+ // Updating the progress and computing the speed require that enough time
+ // passed since the last update, or that we haven't started throttling yet.
+ let currentTimeMs = Date.now();
+ let intervalMs = currentTimeMs - this._lastProgressTimeMs;
+ if (intervalMs >= kProgressUpdateIntervalMs) {
+ // Don't compute the speed unless we started throttling notifications.
+ if (this._lastProgressTimeMs != 0) {
+ // Calculate the speed in bytes per second.
+ let rawSpeed = (aCurrentBytes - this.currentBytes) / intervalMs * 1000;
+ if (this.speed == 0) {
+ // When the previous speed is exactly zero instead of a fractional
+ // number, this can be considered the first element of the series.
+ this.speed = rawSpeed;
+ } else {
+ // Apply exponential smoothing, with a smoothing factor of 0.1.
+ this.speed = rawSpeed * 0.1 + this.speed * 0.9;
+ }
+ }
+
+ // Start throttling notifications only when we have actually received some
+ // bytes for the first time. The timing of the first part of the download
+ // is not reliable, due to possible latency in the initial notifications.
+ // This also allows automated tests to receive and verify the number of
+ // bytes initially transferred.
+ if (aCurrentBytes > 0) {
+ this._lastProgressTimeMs = currentTimeMs;
+
+ // Update the progress now that we don't need its previous value.
+ this.currentBytes = aCurrentBytes;
+ if (this.totalBytes > 0) {
+ this.progress = Math.floor(this.currentBytes / this.totalBytes * 100);
+ }
+ changeMade = true;
}
}
- this._notifyChange();
+
+ if (changeMade) {
+ this._notifyChange();
+ }
},
/**
From 9981fae81fff245b92a4a660ba5494f57eccf8b2 Mon Sep 17 00:00:00 2001
From: Paolo Amadini
Date: Fri, 16 Aug 2013 11:02:18 +0200
Subject: [PATCH 07/46] Bug 836443 - Automatically stop and restart downloads.
r=enn
---
.../jsdownloads/src/DownloadCore.jsm | 150 +++++++++++--
.../jsdownloads/src/DownloadIntegration.jsm | 201 +++++++++++++++++-
.../jsdownloads/src/DownloadList.jsm | 5 +
.../jsdownloads/src/DownloadStore.jsm | 27 ++-
4 files changed, 359 insertions(+), 24 deletions(-)
diff --git a/toolkit/components/jsdownloads/src/DownloadCore.jsm b/toolkit/components/jsdownloads/src/DownloadCore.jsm
index 31007b784042..bb5ae1e62b27 100644
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -203,7 +203,7 @@ Download.prototype = {
*
* This property is relevant while the download is in progress, and also if it
* failed or has been canceled. If the download has been completed
- * successfully, this property is not relevant anymore.
+ * successfully, this property is always false.
*
* Whether partial data can actually be retained depends on the saver and the
* download source, and may not be known before the download is started.
@@ -382,6 +382,7 @@ Download.prototype = {
// Update the status properties for a successful download.
this.progress = 100;
this.succeeded = true;
+ this.hasPartialData = false;
} catch (ex) {
// Fail with a generic status code on cancellation, so that the caller
// is forced to actually check the status properties to see if the
@@ -622,6 +623,47 @@ Download.prototype = {
return this._deferSucceeded.promise;
},
+ /**
+ * Updates the state of a finished, failed, or canceled download based on the
+ * current state in the file system. If the download is in progress or it has
+ * been finalized, this method has no effect, and it returns a resolved
+ * promise.
+ *
+ * This allows the properties of the download to be updated in case the user
+ * moved or deleted the target file or its associated ".part" file.
+ *
+ * @return {Promise}
+ * @resolves When the operation has completed.
+ * @rejects Never.
+ */
+ refresh: function ()
+ {
+ return Task.spawn(function () {
+ if (!this.stopped || this._finalized) {
+ return;
+ }
+
+ // Update the current progress from disk if we retained partial data.
+ if (this.hasPartialData && this.target.partFilePath) {
+ let stat = yield OS.File.stat(this.target.partFilePath);
+
+ // Ignore the result if the state has changed meanwhile.
+ if (!this.stopped || this._finalized) {
+ return;
+ }
+
+ // Update the bytes transferred and the related progress properties.
+ this.currentBytes = stat.size;
+ if (this.totalBytes > 0) {
+ this.hasProgress = true;
+ this.progress = Math.floor(this.currentBytes /
+ this.totalBytes * 100);
+ }
+ this._notifyChange();
+ }
+ }.bind(this)).then(null, Cu.reportError);
+ },
+
/**
* True if the "finalize" method has been called. This prevents the download
* from starting again after having been stopped.
@@ -761,20 +803,54 @@ Download.prototype = {
serializable.saver = saver;
}
- if (this.launcherPath) {
- serializable.launcherPath = this.launcherPath;
+ if (!this.stopped) {
+ serializable.stopped = false;
}
- if (this.launchWhenSucceeded) {
- serializable.launchWhenSucceeded = true;
+ if (this.error && ("message" in this.error)) {
+ serializable.error = { message: this.error.message };
}
- if (this.contentType) {
- serializable.contentType = this.contentType;
+ // These are serialized unless they are false, null, or empty strings.
+ let propertiesToSerialize = [
+ "succeeded",
+ "canceled",
+ "startTime",
+ "totalBytes",
+ "hasPartialData",
+ "tryToKeepPartialData",
+ "launcherPath",
+ "launchWhenSucceeded",
+ "contentType",
+ ];
+
+ for (let property of propertiesToSerialize) {
+ if (this[property]) {
+ serializable[property] = this[property];
+ }
}
return serializable;
},
+
+ /**
+ * Returns a value that changes only when one of the properties of a Download
+ * object that should be saved into a file also change. This excludes
+ * properties whose value doesn't usually change during the download lifetime.
+ *
+ * This function is used to determine whether the download should be
+ * serialized after a property change notification has been received.
+ *
+ * @return String representing the relevant download state.
+ */
+ getSerializationHash: function ()
+ {
+ // The "succeeded", "canceled", "error", and startTime properties are not
+ // taken into account because they all change before the "stopped" property
+ // changes, and are not altered in other cases.
+ return this.stopped + "," + this.totalBytes + "," + this.hasPartialData +
+ "," + this.contentType;
+ },
};
/**
@@ -816,16 +892,28 @@ Download.fromSerializable = function (aSerializable) {
}
download.saver.download = download;
- if ("launchWhenSucceeded" in aSerializable) {
- download.launchWhenSucceeded = !!aSerializable.launchWhenSucceeded;
+ let propertiesToDeserialize = [
+ "startTime",
+ "totalBytes",
+ "hasPartialData",
+ "tryToKeepPartialData",
+ "launcherPath",
+ "launchWhenSucceeded",
+ "contentType",
+ ];
+
+ // If the download should not be restarted automatically, update its state to
+ // reflect success or failure during a previous session.
+ if (!("stopped" in aSerializable) || aSerializable.stopped) {
+ propertiesToDeserialize.push("succeeded");
+ propertiesToDeserialize.push("canceled");
+ propertiesToDeserialize.push("error");
}
- if ("contentType" in aSerializable) {
- download.contentType = aSerializable.contentType;
- }
-
- if ("launcherPath" in aSerializable) {
- download.launcherPath = aSerializable.launcherPath;
+ for (let property of propertiesToDeserialize) {
+ if (property in aSerializable) {
+ download[property] = aSerializable[property];
+ }
}
return download;
@@ -1190,6 +1278,13 @@ DownloadCopySaver.prototype = {
*/
_canceled: false,
+ /**
+ * String corresponding to the entityID property of the nsIResumableChannel
+ * used to execute the download, or null if the channel was not resumable or
+ * the saver was instructed not to keep partially downloaded data.
+ */
+ entityID: null,
+
/**
* Implements "DownloadSaver.execute".
*/
@@ -1428,8 +1523,13 @@ DownloadCopySaver.prototype = {
*/
toSerializable: function ()
{
- // Simplify the representation since we don't have other details for now.
- return "copy";
+ // Simplify the representation if we don't have other details.
+ if (!this.entityID) {
+ return "copy";
+ }
+
+ return { type: "copy",
+ entityID: this.entityID };
},
};
@@ -1443,8 +1543,11 @@ DownloadCopySaver.prototype = {
* @return The newly created DownloadCopySaver object.
*/
DownloadCopySaver.fromSerializable = function (aSerializable) {
- // We don't have other state details for now.
- return new DownloadCopySaver();
+ let saver = new DownloadCopySaver();
+ if ("entityID" in aSerializable) {
+ saver.entityID = aSerializable.entityID;
+ }
+ return saver;
};
////////////////////////////////////////////////////////////////////////////////
@@ -1574,6 +1677,13 @@ DownloadLegacySaver.prototype = {
*/
copySaver: null,
+ /**
+ * String corresponding to the entityID property of the nsIResumableChannel
+ * used to execute the download, or null if the channel was not resumable or
+ * the saver was instructed not to keep partially downloaded data.
+ */
+ entityID: null,
+
/**
* Implements "DownloadSaver.execute".
*/
@@ -1674,7 +1784,7 @@ DownloadLegacySaver.prototype = {
// thus it cannot be rebuilt during deserialization. To support resuming
// across different browser sessions, this object is transformed into a
// DownloadCopySaver for the purpose of serialization.
- return "copy";
+ return DownloadCopySaver.prototype.toSerializable.call(this);
},
};
diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
index cbf48f0454c4..842a1228aa73 100644
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -64,6 +64,31 @@ XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
createBundle("chrome://mozapps/locale/downloads/downloads.properties");
});
+const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
+ "initWithCallback");
+
+/**
+ * Indicates the delay between a change to the downloads data and the related
+ * save operation. This value is the result of a delicate trade-off, assuming
+ * the host application uses the browser history instead of the download store
+ * to save completed downloads.
+ *
+ * If a download takes less than this interval to complete (for example, saving
+ * a page that is already displayed), then no input/output is triggered by the
+ * download store except for an existence check, resulting in the best possible
+ * efficiency.
+ *
+ * Conversely, if the browser is closed before this interval has passed, the
+ * download will not be saved. This prevents it from being restored in the next
+ * session, and if there is partial data associated with it, then the ".part"
+ * file will not be deleted when the browser starts again.
+ *
+ * In all cases, for best efficiency, this value should be high enough that the
+ * input/output for opening or closing the target file does not overlap with the
+ * one for saving the list of downloads.
+ */
+const kSaveDelayMs = 1500;
+
////////////////////////////////////////////////////////////////////////////////
//// DownloadIntegration
@@ -105,7 +130,7 @@ this.DownloadIntegration = {
* @param aList
* DownloadList object to be populated with the download objects
* serialized from the previous session. This list will be persisted
- * to disk during the session lifetime or when the session terminates.
+ * to disk during the session lifetime.
*
* @return {Promise}
* @resolves When the list has been populated.
@@ -124,7 +149,39 @@ this.DownloadIntegration = {
this._store = new DownloadStore(aList, OS.Path.join(
OS.Constants.Path.profileDir,
"downloads.json"));
- return this._store.load();
+ this._store.onsaveitem = this.shouldPersistDownload.bind(this);
+
+ // Load the list of persistent downloads, then add the DownloadAutoSaveView
+ // even if the load operation failed.
+ return this._store.load().then(null, Cu.reportError).then(() => {
+ new DownloadAutoSaveView(aList, this._store);
+ });
+ },
+
+ /**
+ * Determines if a Download object from the list of persistent downloads
+ * should be saved into a file, so that it can be restored across sessions.
+ *
+ * This function allows filtering out downloads that the host application is
+ * not interested in persisting across sessions, for example downloads that
+ * finished successfully.
+ *
+ * @param aDownload
+ * The Download object to be inspected. This is originally taken from
+ * the global DownloadList object for downloads that were not started
+ * from a private browsing window. The item may have been removed
+ * from the list since the save operation started, though in this case
+ * the save operation will be repeated later.
+ *
+ * @return True to save the download, false otherwise.
+ */
+ shouldPersistDownload: function (aDownload)
+ {
+ // In the default implementation, we save all the downloads currently in
+ // progress, as well as stopped downloads for which we retained partially
+ // downloaded data. Stopped downloads for which we don't need to track the
+ // presence of a ".part" file are only retained in the browser history.
+ return aDownload.hasPartialData || !aDownload.stopped;
},
/**
@@ -493,6 +550,10 @@ this.DownloadIntegration = {
* @resolves When the views and observers are added.
*/
addListObservers: function DI_addListObservers(aList, aIsPrivate) {
+ if (this.dontLoad) {
+ return Promise.resolve();
+ }
+
DownloadObserver.registerView(aList, aIsPrivate);
if (!DownloadObserver.observersAdded) {
DownloadObserver.observersAdded = true;
@@ -504,7 +565,10 @@ this.DownloadIntegration = {
}
};
-let DownloadObserver = {
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadObserver
+
+this.DownloadObserver = {
/**
* Flag to determine if the observers have been added previously.
*/
@@ -657,3 +721,134 @@ let DownloadObserver = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference])
};
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadAutoSaveView
+
+/**
+ * This view can be added to a DownloadList object to trigger a save operation
+ * in the given DownloadStore object when a relevant change occurs.
+ *
+ * @param aStore
+ * The DownloadStore object used for saving.
+ */
+function DownloadAutoSaveView(aList, aStore) {
+ this._store = aStore;
+ this._downloadsMap = new Map();
+
+ // We set _initialized to true after adding the view, so that onDownloadAdded
+ // doesn't cause a save to occur.
+ aList.addView(this);
+ this._initialized = true;
+}
+
+DownloadAutoSaveView.prototype = {
+ /**
+ * True when the initial state of the downloads has been loaded.
+ */
+ _initialized: false,
+
+ /**
+ * The DownloadStore object used for saving.
+ */
+ _store: null,
+
+ /**
+ * This map contains only Download objects that should be saved to disk, and
+ * associates them with the result of their getSerializationHash function, for
+ * the purpose of detecting changes to the relevant properties.
+ */
+ _downloadsMap: null,
+
+ /**
+ * This is set to true when the save operation should be triggered. This is
+ * required so that a new operation can be scheduled while the current one is
+ * in progress, without re-entering the save method.
+ */
+ _shouldSave: false,
+
+ /**
+ * nsITimer used for triggering the save operation after a delay, or null if
+ * saving has finished and there is no operation scheduled for execution.
+ *
+ * The logic here is different from the DeferredTask module in that multiple
+ * requests will never delay the operation for longer than the expected time
+ * (no grace delay), and the operation is never re-entered during execution.
+ */
+ _timer: null,
+
+ /**
+ * Timer callback used to serialize the list of downloads.
+ */
+ _save: function ()
+ {
+ Task.spawn(function () {
+ // Any save request received during execution will be handled later.
+ this._shouldSave = false;
+
+ // Execute the asynchronous save operation.
+ try {
+ yield this._store.save();
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ // Handle requests received during the operation.
+ this._timer = null;
+ if (this._shouldSave) {
+ this.saveSoon();
+ }
+ }.bind(this)).then(null, Cu.reportError);
+ },
+
+ /**
+ * Called when the list of downloads changed, this triggers the asynchronous
+ * serialization of the list of downloads.
+ */
+ saveSoon: function ()
+ {
+ this._shouldSave = true;
+ if (!this._timer) {
+ this._timer = new Timer(this._save.bind(this), kSaveDelayMs,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// DownloadList view
+
+ onDownloadAdded: function (aDownload)
+ {
+ if (DownloadIntegration.shouldPersistDownload(aDownload)) {
+ this._downloadsMap.set(aDownload, aDownload.getSerializationHash());
+ if (this._initialized) {
+ this.saveSoon();
+ }
+ }
+ },
+
+ onDownloadChanged: function (aDownload)
+ {
+ if (!DownloadIntegration.shouldPersistDownload(aDownload)) {
+ if (this._downloadsMap.has(aDownload)) {
+ this._downloadsMap.delete(aDownload);
+ this.saveSoon();
+ }
+ return;
+ }
+
+ let hash = aDownload.getSerializationHash();
+ if (this._downloadsMap.get(aDownload) != hash) {
+ this._downloadsMap.set(aDownload, hash);
+ this.saveSoon();
+ }
+ },
+
+ onDownloadRemoved: function (aDownload)
+ {
+ if (this._downloadsMap.has(aDownload)) {
+ this._downloadsMap.delete(aDownload);
+ this.saveSoon();
+ }
+ },
+};
diff --git a/toolkit/components/jsdownloads/src/DownloadList.jsm b/toolkit/components/jsdownloads/src/DownloadList.jsm
index 3f6943e1f1e0..41cbff15dc52 100644
--- a/toolkit/components/jsdownloads/src/DownloadList.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadList.jsm
@@ -171,6 +171,11 @@ DownloadList.prototype = {
* // Called after aDownload is removed from the list.
* },
* }
+ *
+ * @note The onDownloadAdded notifications are sent synchronously. This
+ * allows for a complete initialization of the view used for detecting
+ * changes to downloads to be persisted, before other callers get a
+ * chance to modify them.
*/
addView: function DL_addView(aView)
{
diff --git a/toolkit/components/jsdownloads/src/DownloadStore.jsm b/toolkit/components/jsdownloads/src/DownloadStore.jsm
index 6b3c9bab487e..1a11509e4b91 100644
--- a/toolkit/components/jsdownloads/src/DownloadStore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm
@@ -88,6 +88,12 @@ DownloadStore.prototype = {
*/
path: "",
+ /**
+ * This function is called with a Download object as its first argument, and
+ * should return true if the item should be saved.
+ */
+ onsaveitem: () => true,
+
/**
* Loads persistent downloads from the file to the list.
*
@@ -111,7 +117,23 @@ DownloadStore.prototype = {
// Create live downloads based on the static snapshot.
for (let downloadData of storeData.list) {
try {
- this.list.add(yield Downloads.createDownload(downloadData));
+ let download = yield Downloads.createDownload(downloadData);
+ try {
+ if (("stopped" in downloadData) && !downloadData.stopped) {
+ // Try to restart the download if it was in progress during the
+ // previous session.
+ download.start();
+ } else {
+ // If the download was not in progress, try to update the current
+ // progress from disk. This is relevant in case we retained
+ // partially downloaded data.
+ yield download.refresh();
+ }
+ } finally {
+ // Add the download to the list if we succeeded in creating it,
+ // after we have updated its initial state.
+ this.list.add(download);
+ }
} catch (ex) {
// If an item is unrecognized, don't prevent others from being loaded.
Cu.reportError(ex);
@@ -139,6 +161,9 @@ DownloadStore.prototype = {
let atLeastOneDownload = false;
for (let download of downloads) {
try {
+ if (!this.onsaveitem(download)) {
+ continue;
+ }
storeData.list.push(download.toSerializable());
atLeastOneDownload = true;
} catch (ex) {
From a487207e1f2cb05bf337b0c1870daf3200e5a0a7 Mon Sep 17 00:00:00 2001
From: Paolo Amadini
Date: Fri, 16 Aug 2013 11:02:24 +0200
Subject: [PATCH 08/46] Bug 895403 - Disable methods of nsIDownloadManager that
are replaced by the JavaScript API. r=enn
---
.../downloads/nsDownloadManager.cpp | 85 ++++++++++++++++---
.../components/downloads/nsDownloadManager.h | 1 +
2 files changed, 76 insertions(+), 10 deletions(-)
diff --git a/toolkit/components/downloads/nsDownloadManager.cpp b/toolkit/components/downloads/nsDownloadManager.cpp
index f7e3fabdfd7b..c194a00f1d3d 100644
--- a/toolkit/components/downloads/nsDownloadManager.cpp
+++ b/toolkit/components/downloads/nsDownloadManager.cpp
@@ -36,6 +36,8 @@
#include "SQLFunctions.h"
+#include "mozilla/Preferences.h"
+
#ifdef XP_WIN
#include
#ifdef DOWNLOAD_SCANNER
@@ -60,6 +62,7 @@ using mozilla::downloads::GenerateGUID;
#define DOWNLOAD_MANAGER_BUNDLE "chrome://mozapps/locale/downloads/downloads.properties"
#define DOWNLOAD_MANAGER_ALERT_ICON "chrome://mozapps/skin/downloads/downloadIcon.png"
+#define PREF_BD_USEJSTRANSFER "browser.download.useJSTransfer"
#define PREF_BDM_SHOWALERTONCOMPLETE "browser.download.manager.showAlertOnComplete"
#define PREF_BDM_SHOWALERTINTERVAL "browser.download.manager.showAlertInterval"
#define PREF_BDM_RETENTION "browser.download.manager.retention"
@@ -923,6 +926,21 @@ nsDownloadManager::InitStatements(mozIStorageConnection* aDBConn,
nsresult
nsDownloadManager::Init()
{
+ nsresult rv;
+
+ nsCOMPtr bundleService =
+ mozilla::services::GetStringBundleService();
+ if (!bundleService)
+ return NS_ERROR_FAILURE;
+
+ rv = bundleService->CreateBundle(DOWNLOAD_MANAGER_BUNDLE,
+ getter_AddRefs(mBundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mUseJSTransfer = Preferences::GetBool(PREF_BD_USEJSTRANSFER, false);
+ if (mUseJSTransfer)
+ return NS_OK;
+
// Clean up any old downloads.rdf files from before Firefox 3
{
nsCOMPtr oldDownloadsFile;
@@ -939,16 +957,7 @@ nsDownloadManager::Init()
if (!mObserverService)
return NS_ERROR_FAILURE;
- nsCOMPtr bundleService =
- mozilla::services::GetStringBundleService();
- if (!bundleService)
- return NS_ERROR_FAILURE;
-
- nsresult rv = InitDB();
- NS_ENSURE_SUCCESS(rv, rv);
-
- rv = bundleService->CreateBundle(DOWNLOAD_MANAGER_BUNDLE,
- getter_AddRefs(mBundle));
+ rv = InitDB();
NS_ENSURE_SUCCESS(rv, rv);
#ifdef DOWNLOAD_SCANNER
@@ -1281,6 +1290,8 @@ nsDownloadManager::SendEvent(nsDownload *aDownload, const char *aTopic)
NS_IMETHODIMP
nsDownloadManager::GetActivePrivateDownloadCount(int32_t* aResult)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
*aResult = mCurrentPrivateDownloads.Count();
return NS_OK;
}
@@ -1288,6 +1299,8 @@ nsDownloadManager::GetActivePrivateDownloadCount(int32_t* aResult)
NS_IMETHODIMP
nsDownloadManager::GetActiveDownloadCount(int32_t *aResult)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
*aResult = mCurrentDownloads.Count();
return NS_OK;
@@ -1296,12 +1309,16 @@ nsDownloadManager::GetActiveDownloadCount(int32_t *aResult)
NS_IMETHODIMP
nsDownloadManager::GetActiveDownloads(nsISimpleEnumerator **aResult)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
return NS_NewArrayEnumerator(aResult, mCurrentDownloads);
}
NS_IMETHODIMP
nsDownloadManager::GetActivePrivateDownloads(nsISimpleEnumerator **aResult)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
return NS_NewArrayEnumerator(aResult, mCurrentPrivateDownloads);
}
@@ -1523,6 +1540,8 @@ nsDownloadManager::AddDownload(DownloadType aDownloadType,
bool aIsPrivate,
nsIDownload **aDownload)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
NS_ENSURE_ARG_POINTER(aSource);
NS_ENSURE_ARG_POINTER(aTarget);
NS_ENSURE_ARG_POINTER(aDownload);
@@ -1655,6 +1674,8 @@ nsDownloadManager::AddDownload(DownloadType aDownloadType,
NS_IMETHODIMP
nsDownloadManager::GetDownload(uint32_t aID, nsIDownload **aDownloadItem)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
NS_WARNING("Using integer IDs without compat mode enabled");
nsDownload *itm = FindDownload(aID);
@@ -1699,6 +1720,8 @@ NS_IMETHODIMP
nsDownloadManager::GetDownloadByGUID(const nsACString& aGUID,
nsIDownloadManagerResult* aCallback)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
nsDownload *itm = FindDownload(aGUID);
nsresult rv = NS_OK;
@@ -1748,6 +1771,8 @@ nsDownloadManager::FindDownload(const nsACString& aGUID)
NS_IMETHODIMP
nsDownloadManager::CancelDownload(uint32_t aID)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
NS_WARNING("Using integer IDs without compat mode enabled");
// We AddRef here so we don't lose access to member variables when we remove
@@ -1774,6 +1799,8 @@ nsDownloadManager::RetryDownload(const nsACString& aGUID)
NS_IMETHODIMP
nsDownloadManager::RetryDownload(uint32_t aID)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
NS_WARNING("Using integer IDs without compat mode enabled");
nsRefPtr dl;
@@ -1876,6 +1903,8 @@ nsDownloadManager::RemoveDownload(const nsACString& aGUID)
NS_IMETHODIMP
nsDownloadManager::RemoveDownload(uint32_t aID)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
NS_WARNING("Using integer IDs without compat mode enabled");
nsRefPtr dl = FindDownload(aID);
@@ -1982,6 +2011,8 @@ NS_IMETHODIMP
nsDownloadManager::RemoveDownloadsByTimeframe(int64_t aStartTime,
int64_t aEndTime)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
nsresult rv = DoRemoveDownloadsByTimeframe(mDBConn, aStartTime, aEndTime);
nsresult rv2 = DoRemoveDownloadsByTimeframe(mPrivateDBConn, aStartTime, aEndTime);
NS_ENSURE_SUCCESS(rv, rv);
@@ -1994,12 +2025,16 @@ nsDownloadManager::RemoveDownloadsByTimeframe(int64_t aStartTime,
NS_IMETHODIMP
nsDownloadManager::CleanUp()
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
return CleanUp(mDBConn);
}
NS_IMETHODIMP
nsDownloadManager::CleanUpPrivate()
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
return CleanUp(mPrivateDBConn);
}
@@ -2083,18 +2118,24 @@ DoGetCanCleanUp(mozIStorageConnection* aDBConn, bool *aResult)
NS_IMETHODIMP
nsDownloadManager::GetCanCleanUp(bool *aResult)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
return DoGetCanCleanUp(mDBConn, aResult);
}
NS_IMETHODIMP
nsDownloadManager::GetCanCleanUpPrivate(bool *aResult)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
return DoGetCanCleanUp(mPrivateDBConn, aResult);
}
NS_IMETHODIMP
nsDownloadManager::PauseDownload(uint32_t aID)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
NS_WARNING("Using integer IDs without compat mode enabled");
nsDownload *dl = FindDownload(aID);
@@ -2107,6 +2148,8 @@ nsDownloadManager::PauseDownload(uint32_t aID)
NS_IMETHODIMP
nsDownloadManager::ResumeDownload(uint32_t aID)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
NS_WARNING("Using integer IDs without compat mode enabled");
nsDownload *dl = FindDownload(aID);
@@ -2119,6 +2162,8 @@ nsDownloadManager::ResumeDownload(uint32_t aID)
NS_IMETHODIMP
nsDownloadManager::GetDBConnection(mozIStorageConnection **aDBConn)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
NS_ADDREF(*aDBConn = mDBConn);
return NS_OK;
@@ -2127,6 +2172,8 @@ nsDownloadManager::GetDBConnection(mozIStorageConnection **aDBConn)
NS_IMETHODIMP
nsDownloadManager::GetPrivateDBConnection(mozIStorageConnection **aDBConn)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
NS_ADDREF(*aDBConn = mPrivateDBConn);
return NS_OK;
@@ -2135,6 +2182,8 @@ nsDownloadManager::GetPrivateDBConnection(mozIStorageConnection **aDBConn)
NS_IMETHODIMP
nsDownloadManager::AddListener(nsIDownloadProgressListener *aListener)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
mListeners.AppendObject(aListener);
return NS_OK;
}
@@ -2142,6 +2191,8 @@ nsDownloadManager::AddListener(nsIDownloadProgressListener *aListener)
NS_IMETHODIMP
nsDownloadManager::AddPrivacyAwareListener(nsIDownloadProgressListener *aListener)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
mPrivacyAwareListeners.AppendObject(aListener);
return NS_OK;
}
@@ -2149,6 +2200,8 @@ nsDownloadManager::AddPrivacyAwareListener(nsIDownloadProgressListener *aListene
NS_IMETHODIMP
nsDownloadManager::RemoveListener(nsIDownloadProgressListener *aListener)
{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
mListeners.RemoveObject(aListener);
mPrivacyAwareListeners.RemoveObject(aListener);
return NS_OK;
@@ -2231,6 +2284,10 @@ nsDownloadManager::NotifyListenersOnStateChange(nsIWebProgress *aProgress,
NS_IMETHODIMP
nsDownloadManager::OnBeginUpdateBatch()
{
+ // This method in not normally invoked when mUseJSTransfer is enabled, however
+ // we provide an extra check in case it is called manually by add-ons.
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
// We already have a transaction, so don't make another
if (mHistoryTransaction)
return NS_OK;
@@ -2272,6 +2329,10 @@ nsDownloadManager::OnDeleteURI(nsIURI *aURI,
const nsACString& aGUID,
uint16_t aReason)
{
+ // This method in not normally invoked when mUseJSTransfer is enabled, however
+ // we provide an extra check in case it is called manually by add-ons.
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
nsresult rv = RemoveDownloadsForURI(mGetIdsForURIStatement, aURI);
nsresult rv2 = RemoveDownloadsForURI(mGetPrivateIdsForURIStatement, aURI);
NS_ENSURE_SUCCESS(rv, rv);
@@ -2311,6 +2372,10 @@ nsDownloadManager::Observe(nsISupports *aSubject,
const char *aTopic,
const PRUnichar *aData)
{
+ // This method in not normally invoked when mUseJSTransfer is enabled, however
+ // we provide an extra check in case it is called manually by add-ons.
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
// We need to count the active public downloads that could be lost
// by quitting, and add any active private ones as well, since per-window
// private browsing may be active.
diff --git a/toolkit/components/downloads/nsDownloadManager.h b/toolkit/components/downloads/nsDownloadManager.h
index 6597416be9ca..6377ae1bfe58 100644
--- a/toolkit/components/downloads/nsDownloadManager.h
+++ b/toolkit/components/downloads/nsDownloadManager.h
@@ -242,6 +242,7 @@ private:
nsresult ResumeAllDownloads(nsCOMArray& aDownloads, bool aResumeAll);
nsresult RemoveDownloadsForURI(mozIStorageStatement* aStatement, nsIURI *aURI);
+ bool mUseJSTransfer;
nsCOMArray mListeners;
nsCOMArray mPrivacyAwareListeners;
nsCOMPtr mBundle;
From 67889faa26a4713b1e984dcd3ebeaa7ca79b53c5 Mon Sep 17 00:00:00 2001
From: Kartikaya Gupta
Date: Fri, 16 Aug 2013 08:42:23 -0400
Subject: [PATCH 09/46] Bug 898877 - Prevent pages from getting stuck without
the dynamic toolbar. r=Cwiiis
The problematic scenario is when the page is exactly the height of the screen
(with dynamic toolbar not visible). In this case, the scrollable() function in
Axis.java returns false on the vertical axis, and so the JavaPanZoomController
never does any scrolling. This in turns means that the scrollBy code in
LayerMarginsAnimator never gets to run, so you can never drag the toolbar back
into being visible. The patch ensures that scrollable() returns true when some
or all of the margins are not visible, ensuring that in these scenarios the
user can still scroll the toolbar back onto the screen. This patch also adds
some comments/asserts to verify the new code is threadsafe.
---
mobile/android/base/gfx/Axis.java | 8 ++++++++
mobile/android/base/gfx/GeckoLayerClient.java | 6 ++++++
mobile/android/base/gfx/JavaPanZoomController.java | 12 ++++++++++++
mobile/android/base/gfx/LayerMarginsAnimator.java | 11 ++++++++++-
mobile/android/base/gfx/PanZoomTarget.java | 2 ++
5 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/mobile/android/base/gfx/Axis.java b/mobile/android/base/gfx/Axis.java
index 7f6c3cacc27b..cf7db8fe0c9d 100644
--- a/mobile/android/base/gfx/Axis.java
+++ b/mobile/android/base/gfx/Axis.java
@@ -145,6 +145,7 @@ abstract class Axis {
protected abstract float getViewportLength();
protected abstract float getPageStart();
protected abstract float getPageLength();
+ protected abstract boolean marginsHidden();
Axis(SubdocumentScrollHelper subscroller) {
mSubscroller = subscroller;
@@ -253,6 +254,13 @@ abstract class Axis {
return false;
}
+ // if there are margins on this axis but they are currently hidden,
+ // we must be able to scroll in order to make them visible, so allow
+ // scrolling in that case
+ if (marginsHidden()) {
+ return true;
+ }
+
// there is scrollable space, and we're not disabled, or the document fits the viewport
// but we always allow overscroll anyway
return getViewportLength() <= getPageLength() - MIN_SCROLLABLE_DISTANCE ||
diff --git a/mobile/android/base/gfx/GeckoLayerClient.java b/mobile/android/base/gfx/GeckoLayerClient.java
index 97c0c0b1e968..165f549b23ac 100644
--- a/mobile/android/base/gfx/GeckoLayerClient.java
+++ b/mobile/android/base/gfx/GeckoLayerClient.java
@@ -776,6 +776,12 @@ public class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
return mView.isFullScreen();
}
+ /** Implementation of PanZoomTarget */
+ @Override
+ public RectF getMaxMargins() {
+ return mMarginsAnimator.getMaxMargins();
+ }
+
/** Implementation of PanZoomTarget */
@Override
public void setAnimationTarget(ImmutableViewportMetrics metrics) {
diff --git a/mobile/android/base/gfx/JavaPanZoomController.java b/mobile/android/base/gfx/JavaPanZoomController.java
index 99815a8700c8..ce410258323a 100644
--- a/mobile/android/base/gfx/JavaPanZoomController.java
+++ b/mobile/android/base/gfx/JavaPanZoomController.java
@@ -1056,6 +1056,12 @@ class JavaPanZoomController
protected float getPageStart() { return getMetrics().pageRectLeft; }
@Override
protected float getPageLength() { return getMetrics().getPageWidthWithMargins(); }
+ @Override
+ protected boolean marginsHidden() {
+ ImmutableViewportMetrics metrics = getMetrics();
+ RectF maxMargins = mTarget.getMaxMargins();
+ return (metrics.marginLeft < maxMargins.left || metrics.marginRight < maxMargins.right);
+ }
}
private class AxisY extends Axis {
@@ -1068,6 +1074,12 @@ class JavaPanZoomController
protected float getPageStart() { return getMetrics().pageRectTop; }
@Override
protected float getPageLength() { return getMetrics().getPageHeightWithMargins(); }
+ @Override
+ protected boolean marginsHidden() {
+ ImmutableViewportMetrics metrics = getMetrics();
+ RectF maxMargins = mTarget.getMaxMargins();
+ return (metrics.marginTop < maxMargins.top || metrics.marginBottom < maxMargins.bottom);
+ }
}
/*
diff --git a/mobile/android/base/gfx/LayerMarginsAnimator.java b/mobile/android/base/gfx/LayerMarginsAnimator.java
index ac170c6bbd80..c2613a071757 100644
--- a/mobile/android/base/gfx/LayerMarginsAnimator.java
+++ b/mobile/android/base/gfx/LayerMarginsAnimator.java
@@ -10,6 +10,7 @@ import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.TouchEventInterceptor;
import org.mozilla.gecko.util.FloatUtils;
+import org.mozilla.gecko.util.ThreadUtils;
import android.graphics.PointF;
import android.graphics.RectF;
@@ -33,7 +34,9 @@ public class LayerMarginsAnimator implements TouchEventInterceptor {
*/
private float SHOW_MARGINS_THRESHOLD = 0.20f;
- /* This rect stores the maximum value margins can grow to when scrolling */
+ /* This rect stores the maximum value margins can grow to when scrolling. When writing
+ * to this member variable, or when reading from this member variable on a non-UI thread,
+ * you must synchronize on the LayerMarginsAnimator instance. */
private final RectF mMaxMargins;
/* If this boolean is true, scroll changes will not affect margins */
private boolean mMarginsPinned;
@@ -86,6 +89,8 @@ public class LayerMarginsAnimator implements TouchEventInterceptor {
* Sets the maximum values for margins to grow to, in pixels.
*/
public synchronized void setMaxMargins(float left, float top, float right, float bottom) {
+ ThreadUtils.assertOnUiThread();
+
mMaxMargins.set(left, top, right, bottom);
// Update the Gecko-side global for fixed viewport margins.
@@ -95,6 +100,10 @@ public class LayerMarginsAnimator implements TouchEventInterceptor {
+ ", \"bottom\" : " + bottom + ", \"left\" : " + left + " }"));
}
+ RectF getMaxMargins() {
+ return mMaxMargins;
+ }
+
private void animateMargins(final float left, final float top, final float right, final float bottom, boolean immediately) {
if (mAnimationTimer != null) {
mAnimationTimer.cancel();
diff --git a/mobile/android/base/gfx/PanZoomTarget.java b/mobile/android/base/gfx/PanZoomTarget.java
index d0df1057a15b..ee932557b322 100644
--- a/mobile/android/base/gfx/PanZoomTarget.java
+++ b/mobile/android/base/gfx/PanZoomTarget.java
@@ -8,11 +8,13 @@ package org.mozilla.gecko.gfx;
import org.mozilla.gecko.ZoomConstraints;
import android.graphics.PointF;
+import android.graphics.RectF;
public interface PanZoomTarget {
public ImmutableViewportMetrics getViewportMetrics();
public ZoomConstraints getZoomConstraints();
public boolean isFullScreen();
+ public RectF getMaxMargins();
public void setAnimationTarget(ImmutableViewportMetrics viewport);
public void setViewportMetrics(ImmutableViewportMetrics viewport);
From d802b5ccca838c5a86e0bb3a117d062d378e3124 Mon Sep 17 00:00:00 2001
From: Kartikaya Gupta
Date: Fri, 16 Aug 2013 08:42:32 -0400
Subject: [PATCH 10/46] Bug 898877 - Ensure that viewport resizing behaviour
due to dynamic toolbar is consistent. r=Cwiiis
In browser.js were two pieces of code that determined whether or not the CSS viewport
size should include the margins. These two pieces of code were inconsistent in that
one used rounding while the other used a fuzz. Also, one of them subtracted gViewportMargins
from gScreenHeight while the other added them. This patch makes the two pieces of code
consistent, and updates them to use a fuzz so that the CSS viewport is enlarged only
when dealing with pages that are equal to or larger than the screen (with the toolbar hidden).
---
mobile/android/chrome/content/browser.js | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js
index b0a7d2a55f3a..006cb27768c7 100644
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -3276,15 +3276,13 @@ Tab.prototype = {
let remeasureNeeded = false;
if (hasHorizontalMargins) {
- let screenWidth = gScreenWidth + gViewportMargins.left + gViewportMargins.right;
- let viewportShouldExcludeHorizontalMargins = (Math.round(pageWidth) <= screenWidth);
+ let viewportShouldExcludeHorizontalMargins = (pageWidth <= gScreenWidth - 0.5);
if (viewportShouldExcludeHorizontalMargins != this.viewportExcludesHorizontalMargins) {
remeasureNeeded = true;
}
}
if (hasVerticalMargins) {
- let screenHeight = gScreenHeight + gViewportMargins.top + gViewportMargins.bottom;
- let viewportShouldExcludeVerticalMargins = (Math.round(pageHeight) <= screenHeight);
+ let viewportShouldExcludeVerticalMargins = (pageHeight <= gScreenHeight - 0.5);
if (viewportShouldExcludeVerticalMargins != this.viewportExcludesVerticalMargins) {
remeasureNeeded = true;
}
From 60789e432675de0ce64335eb5581acacfc54f3c3 Mon Sep 17 00:00:00 2001
From: Mike Hordecki
Date: Fri, 16 Aug 2013 08:48:22 -0400
Subject: [PATCH 11/46] Bug 755412 - Debugging protocol server should drop
connection if packet framing is bad. r=past, r=jimb
---
.../unit/test_dbgsocket_connection_drop.js | 73 +++++++++++++++++++
.../devtools/server/tests/unit/xpcshell.ini | 1 +
toolkit/devtools/server/transport.js | 14 +++-
3 files changed, 87 insertions(+), 1 deletion(-)
create mode 100644 toolkit/devtools/server/tests/unit/test_dbgsocket_connection_drop.js
diff --git a/toolkit/devtools/server/tests/unit/test_dbgsocket_connection_drop.js b/toolkit/devtools/server/tests/unit/test_dbgsocket_connection_drop.js
new file mode 100644
index 000000000000..be97a5cce523
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_dbgsocket_connection_drop.js
@@ -0,0 +1,73 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Bug 755412 - checks if the server drops the connection on an improperly
+ * framed packet, i.e. when the length header is invalid.
+ */
+
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
+
+let port = 2929;
+
+function run_test()
+{
+ do_print("Starting test at " + new Date().toTimeString());
+ initTestDebuggerServer();
+
+ add_test(test_socket_conn_drops_after_invalid_header);
+ add_test(test_socket_conn_drops_after_invalid_header_2);
+ add_test(test_socket_conn_drops_after_too_long_header);
+ run_next_test();
+}
+
+function test_socket_conn_drops_after_invalid_header() {
+ return test_helper('fluff30:27:{"to":"root","type":"echo"}');
+}
+
+function test_socket_conn_drops_after_invalid_header_2() {
+ return test_helper('27asd:{"to":"root","type":"echo"}');
+}
+
+function test_socket_conn_drops_after_too_long_header() {
+ return test_helper('4305724038957487634549823475894325');
+}
+
+
+function test_helper(payload) {
+ try_open_listener();
+
+ let transport = debuggerSocketConnect("127.0.0.1", port);
+ transport.hooks = {
+ onPacket: function(aPacket) {
+ this.onPacket = function(aPacket) {
+ do_throw(new Error("This connection should be dropped."));
+ transport.close();
+ }
+
+ // Inject the payload directly into the stream.
+ transport._outgoing += payload;
+ transport._flushOutgoing();
+ },
+ onClosed: function(aStatus) {
+ do_check_true(true);
+ run_next_test();
+ },
+ };
+ transport.ready();
+}
+
+function try_open_listener()
+{
+ try {
+ do_check_true(DebuggerServer.openListener(port));
+ } catch (e) {
+ // In case the port is unavailable, pick a random one between 2000 and 65000.
+ port = Math.floor(Math.random() * (65000 - 2000 + 1)) + 2000;
+ try_open_listener();
+ }
+}
diff --git a/toolkit/devtools/server/tests/unit/xpcshell.ini b/toolkit/devtools/server/tests/unit/xpcshell.ini
index 479485e88e0b..1d997db9ad8e 100644
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -8,6 +8,7 @@ tail =
[test_dbgsocket.js]
skip-if = toolkit == "gonk"
reason = bug 821285
+[test_dbgsocket_connection_drop.js]
[test_dbgactor.js]
[test_dbgglobal.js]
[test_dbgclient_debuggerstatement.js]
diff --git a/toolkit/devtools/server/transport.js b/toolkit/devtools/server/transport.js
index 72d62a57d7df..f51dfc0310be 100644
--- a/toolkit/devtools/server/transport.js
+++ b/toolkit/devtools/server/transport.js
@@ -151,10 +151,22 @@ DebuggerTransport.prototype = {
// Well this is ugly.
let sep = this._incoming.indexOf(':');
if (sep < 0) {
+ // Incoming packet length is too big anyway - drop the connection.
+ if (this._incoming.length > 20) {
+ this.close();
+ }
+
return false;
}
- let count = parseInt(this._incoming.substring(0, sep));
+ let count = this._incoming.substring(0, sep);
+ // Check for a positive number with no garbage afterwards.
+ if (!/^[0-9]+$/.exec(count)) {
+ this.close();
+ return false;
+ }
+
+ count = +count;
if (this._incoming.length - (sep + 1) < count) {
// Don't have a complete request yet.
return false;
From a9a0c70de9d1050c03191ed53b3410bb13ff3415 Mon Sep 17 00:00:00 2001
From: Nicolas Carlo
Date: Fri, 16 Aug 2013 08:48:39 -0400
Subject: [PATCH 12/46] Bug 892942 - Remove www., m. and mobile. from URLs in
Reader Mode. r=margaret
---
mobile/android/chrome/content/aboutReader.js | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/mobile/android/chrome/content/aboutReader.js b/mobile/android/chrome/content/aboutReader.js
index 16cace6db7a8..c6d7a8c9ecf2 100644
--- a/mobile/android/chrome/content/aboutReader.js
+++ b/mobile/android/chrome/content/aboutReader.js
@@ -580,6 +580,23 @@ AboutReader.prototype = {
this._doc.title = error;
},
+ // This function is the JS version of Java's StringUtils.stripCommonSubdomains.
+ _stripHost: function Reader_stripHost(host) {
+ if (!host)
+ return host;
+
+ let start = 0;
+
+ if (host.startsWith("www"))
+ start = 4;
+ else if (host.startsWith("m"))
+ start = 2;
+ else if (host.startsWith("mobile"))
+ start = 7;
+
+ return host.substring(start);
+ },
+
_showContent: function Reader_showContent(article) {
this._messageElement.style.display = "none";
@@ -587,7 +604,7 @@ AboutReader.prototype = {
this._domainElement.href = article.url;
let articleUri = Services.io.newURI(article.url, null, null);
- this._domainElement.innerHTML = articleUri.host;
+ this._domainElement.innerHTML = this._stripHost(articleUri.host);
this._creditsElement.innerHTML = article.byline;
From 9d9fb81235bcd114696a84e276d3e3f49a49979a Mon Sep 17 00:00:00 2001
From: Yury Delendik
Date: Tue, 13 Aug 2013 17:13:19 -0500
Subject: [PATCH 13/46] Bug 894546 - Histograms for pdf.js. r=froydnj
---
toolkit/components/telemetry/Histograms.json | 44 ++++++++++++++++++++
1 file changed, 44 insertions(+)
diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json
index 91df9db1a61b..ed63748a7c79 100644
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -3153,6 +3153,50 @@
"n_buckets": 15,
"description": "Time (ms) for a WAL checkpoint after collecting all measurements."
},
+ "PDF_VIEWER_USED": {
+ "kind": "boolean",
+ "description": "How many times PDF Viewer was used"
+ },
+ "PDF_VIEWER_FALLBACK_SHOWN": {
+ "kind": "boolean",
+ "description": "How many times PDF Viewer fallback bar was shown"
+ },
+ "PDF_VIEWER_PRINT": {
+ "kind": "boolean",
+ "description": "How many times PDF Viewer print functionality was used"
+ },
+ "PDF_VIEWER_DOCUMENT_VERSION": {
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "The PDF document version (1.1, 1.2, etc.)"
+ },
+ "PDF_VIEWER_DOCUMENT_GENERATOR": {
+ "kind": "enumerated",
+ "n_values": 30,
+ "description": "The PDF document generator"
+ },
+ "PDF_VIEWER_DOCUMENT_SIZE_KB": {
+ "kind": "exponential",
+ "low": "2",
+ "high": "64 * 1024",
+ "n_buckets": 20,
+ "description": "The PDF document size (KB)"
+ },
+ "PDF_VIEWER_FORM": {
+ "kind": "boolean",
+ "description": "A PDF form expected: true for AcroForm and false for XFA"
+ },
+ "PDF_VIEWER_STREAM_TYPES": {
+ "kind": "enumerated",
+ "n_values": 9,
+ "description": "The PDF document compression stream types used"
+ },
+ "PDF_VIEWER_TIME_TO_VIEW_MS": {
+ "kind": "exponential",
+ "high": "10000",
+ "n_buckets": 50,
+ "description": "Time spent to display first page in PDF Viewer (ms)"
+ },
"POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS": {
"kind": "linear",
"low": 25,
From d286185ae55b31700a24dc8a4e2346769180bc1c Mon Sep 17 00:00:00 2001
From: Jared Wein
Date: Fri, 16 Aug 2013 14:03:43 -0400
Subject: [PATCH 14/46] Bug 897160 - Set a minimum width for the Firefox
window. r=mconley
---
browser/base/content/browser.css | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
index 1557205137bd..d76cfb125464 100644
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -5,6 +5,14 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
+#main-window:not([chromehidden~="toolbar"]) {
+%ifdef XP_MACOSX
+ min-width: 425px;
+%else
+ min-width: 390px;
+%endif
+}
+
searchbar {
-moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
}
From 57618a9e56597613e14181ae36bf219151c7ee51 Mon Sep 17 00:00:00 2001
From: Felipe Gomes
Date: Fri, 16 Aug 2013 15:35:38 -0300
Subject: [PATCH 15/46] No bug - Add toolkit/components/jsdownloads to
dumbmake-dependencies. rs=paolo
---
build/dumbmake-dependencies | 1 +
1 file changed, 1 insertion(+)
diff --git a/build/dumbmake-dependencies b/build/dumbmake-dependencies
index f750884ae00c..a21759e0ca5d 100644
--- a/build/dumbmake-dependencies
+++ b/build/dumbmake-dependencies
@@ -58,6 +58,7 @@ browser/app
browser/themes
toolkit
toolkit/components
+ toolkit/components/jsdownloads
toolkit/content
toolkit/crashreporter
toolkit/devtools
From a2d8d8172f9c79a6321639aa353184d495a4e454 Mon Sep 17 00:00:00 2001
From: Felipe Gomes
Date: Fri, 16 Aug 2013 15:35:42 -0300
Subject: [PATCH 16/46] Bug 851466 - Import downloads.sqlite to downloads.json.
r=paolo
---
.../jsdownloads/src/DownloadCore.jsm | 2 +-
.../jsdownloads/src/DownloadImport.jsm | 188 ++++++++++++++++++
.../jsdownloads/src/DownloadIntegration.jsm | 91 +++++++--
.../components/jsdownloads/src/Downloads.jsm | 2 +-
toolkit/components/jsdownloads/src/moz.build | 1 +
5 files changed, 264 insertions(+), 20 deletions(-)
create mode 100644 toolkit/components/jsdownloads/src/DownloadImport.jsm
diff --git a/toolkit/components/jsdownloads/src/DownloadCore.jsm b/toolkit/components/jsdownloads/src/DownloadCore.jsm
index bb5ae1e62b27..fec6800961d2 100644
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -1107,7 +1107,7 @@ function DownloadError(aResult, aMessage, aInferCause)
if (aMessage) {
this.message = aMessage;
} else {
- let exception = new Components.Exception(this.result);
+ let exception = new Components.Exception("", this.result);
this.message = exception.toString();
}
if (aInferCause) {
diff --git a/toolkit/components/jsdownloads/src/DownloadImport.jsm b/toolkit/components/jsdownloads/src/DownloadImport.jsm
new file mode 100644
index 000000000000..e9ee1226afc7
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadImport.jsm
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadImport",
+];
+
+////////////////////////////////////////////////////////////////////////////////
+//// 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, "OS",
+ "resource://gre/modules/osfile.jsm")
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+/**
+ * These values come from the previous interface
+ * nsIDownloadManager, which has now been deprecated.
+ * These are the only types of download states that
+ * we will import.
+ */
+const DOWNLOAD_NOTSTARTED = -1;
+const DOWNLOAD_DOWNLOADING = 0;
+const DOWNLOAD_PAUSED = 4;
+const DOWNLOAD_QUEUED = 5;
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadImport
+
+/**
+ * Provides an object that has a method to import downloads
+ * from the previous SQLite storage format.
+ *
+ * @param aList A DownloadList where each successfully
+ * imported download will be added.
+ * @param aPath The path to the database file.
+ */
+this.DownloadImport = function(aList, aPath) {
+ this.list = aList;
+ this.path = aPath;
+}
+
+this.DownloadImport.prototype = {
+ /**
+ * Imports unfinished downloads from the previous SQLite storage
+ * format (supporting schemas 7 and up), to the new Download object
+ * format. Each imported download will be added to the DownloadList
+ *
+ * @return {Promise}
+ * @resolves When the operation has completed (i.e., every download
+ * from the previous database has been read and added to
+ * the DownloadList)
+ */
+ import: function () {
+ return Task.spawn(function task_DI_import() {
+ let connection = yield Sqlite.openConnection({ path: this.path });
+
+ try {
+ let schemaVersion = yield connection.getSchemaVersion();
+ // We don't support schemas older than version 7 (from 2007)
+ // - Version 7 added the columns mimeType, preferredApplication
+ // and preferredAction in 2007
+ // - Version 8 added the column autoResume in 2007
+ // (if we encounter version 7 we will treat autoResume = false)
+ // - Version 9 is the last known version, which added a unique
+ // GUID text column that is not used here
+ if (schemaVersion < 7) {
+ throw new Error("Unable to import in-progress downloads because "
+ + "the existing profile is too old.");
+ }
+
+ let rows = yield connection.execute("SELECT * FROM moz_downloads");
+
+ for (let row of rows) {
+ try {
+ // Get the DB row data
+ let source = row.getResultByName("source");
+ let target = row.getResultByName("target");
+ let tempPath = row.getResultByName("tempPath");
+ let startTime = row.getResultByName("startTime");
+ let state = row.getResultByName("state");
+ let referrer = row.getResultByName("referrer");
+ let maxBytes = row.getResultByName("maxBytes");
+ let mimeType = row.getResultByName("mimeType");
+ let preferredApplication = row.getResultByName("preferredApplication");
+ let preferredAction = row.getResultByName("preferredAction");
+ let entityID = row.getResultByName("entityID");
+
+ let autoResume = false;
+ try {
+ autoResume = row.getResultByName("autoResume");
+ } catch (ex) {
+ // autoResume wasn't present in schema version 7
+ }
+
+ if (!source) {
+ throw new Error("Attempted to import a row with an empty " +
+ "source column.");
+ }
+
+ let resumeDownload = false;
+
+ switch (state) {
+ case DOWNLOAD_NOTSTARTED:
+ case DOWNLOAD_QUEUED:
+ case DOWNLOAD_DOWNLOADING:
+ resumeDownload = true;
+ break;
+
+ case DOWNLOAD_PAUSED:
+ resumeDownload = autoResume;
+ break;
+
+ default:
+ // We won't import downloads in other states
+ continue;
+ }
+
+ // Transform the data
+ let targetPath = NetUtil.newURI(target)
+ .QueryInterface(Ci.nsIFileURL).path;
+
+ let launchWhenSucceeded = (preferredAction != Ci.nsIMIMEInfo.saveToDisk);
+
+ let downloadOptions = {
+ source: {
+ url: source,
+ referrer: referrer
+ },
+ target: {
+ path: targetPath,
+ partFilePath: tempPath,
+ },
+ saver: {
+ type: "copy",
+ entityID: entityID
+ },
+ startTime: startTime,
+ totalBytes: maxBytes,
+ hasPartialData: true, // true because it's a paused download
+ tryToKeepPartialData: true,
+ launchWhenSucceeded: launchWhenSucceeded,
+ contentType: mimeType,
+ launcherPath: preferredApplication
+ };
+
+ let download = yield Downloads.createDownload(downloadOptions);
+
+ this.list.add(download);
+
+ if (resumeDownload) {
+ download.start();
+ } else {
+ yield download.refresh();
+ }
+
+ } catch (ex) {
+ Cu.reportError("Error importing download: " + ex);
+ }
+ }
+
+ } catch (ex) {
+ Cu.reportError(ex);
+ } finally {
+ yield connection.close();
+ }
+ }.bind(this));
+ }
+}
+
diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
index 842a1228aa73..abf660de8c3e 100644
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -27,6 +27,8 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
"resource://gre/modules/DownloadStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
+ "resource://gre/modules/DownloadImport.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
@@ -89,6 +91,12 @@ const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
*/
const kSaveDelayMs = 1500;
+/**
+ * This pref indicates if we have already imported (or attempted to import)
+ * the downloads database from the previous SQLite storage.
+ */
+const kPrefImportedFromSqlite = "browser.download.importedFromSqlite";
+
////////////////////////////////////////////////////////////////////////////////
//// DownloadIntegration
@@ -136,26 +144,59 @@ this.DownloadIntegration = {
* @resolves When the list has been populated.
* @rejects JavaScript exception.
*/
- loadPersistent: function DI_loadPersistent(aList)
- {
- if (this.dontLoad) {
- return Promise.resolve();
- }
+ initializePublicDownloadList: function(aList) {
+ return Task.spawn(function task_DI_initializePublicDownloadList() {
+ if (this.dontLoad) {
+ return;
+ }
- if (this._store) {
- throw new Error("loadPersistent may be called only once.");
- }
+ if (this._store) {
+ throw new Error("initializePublicDownloadList may be called only once.");
+ }
- this._store = new DownloadStore(aList, OS.Path.join(
- OS.Constants.Path.profileDir,
- "downloads.json"));
- this._store.onsaveitem = this.shouldPersistDownload.bind(this);
+ this._store = new DownloadStore(aList, OS.Path.join(
+ OS.Constants.Path.profileDir,
+ "downloads.json"));
+ this._store.onsaveitem = this.shouldPersistDownload.bind(this);
- // Load the list of persistent downloads, then add the DownloadAutoSaveView
- // even if the load operation failed.
- return this._store.load().then(null, Cu.reportError).then(() => {
+ if (this._importedFromSqlite) {
+ try {
+ yield this._store.load();
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ } else {
+ let sqliteDBpath = OS.Path.join(OS.Constants.Path.profileDir,
+ "downloads.sqlite");
+
+ if (yield OS.File.exists(sqliteDBpath)) {
+ let sqliteImport = new DownloadImport(aList, sqliteDBpath);
+ yield sqliteImport.import();
+
+ let importCount = (yield aList.getAll()).length;
+ if (importCount > 0) {
+ try {
+ yield this._store.save();
+ } catch (ex) { }
+ }
+
+ // No need to wait for the file removal.
+ OS.File.remove(sqliteDBpath).then(null, Cu.reportError);
+ }
+
+ Services.prefs.setBoolPref(kPrefImportedFromSqlite, true);
+
+ // Don't even report error here because this file is pre Firefox 3
+ // and most likely doesn't exist.
+ OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir,
+ "downloads.rdf"));
+
+ }
+
+ // After the list of persisten downloads have been loaded, add
+ // the DownloadAutoSaveView (even if the load operation failed).
new DownloadAutoSaveView(aList, this._store);
- });
+ }.bind(this));
},
/**
@@ -196,7 +237,7 @@ this.DownloadIntegration = {
// This explicitly makes this function a generator for Task.jsm. We
// need this because calls to the "yield" operator below may be
// preprocessed out on some platforms.
- yield;
+ yield undefined;
throw new Task.Result(this._downloadsDirectory);
}
@@ -562,7 +603,21 @@ this.DownloadIntegration = {
Services.obs.addObserver(DownloadObserver, "last-pb-context-exiting", true);
}
return Promise.resolve();
- }
+ },
+
+ /**
+ * Checks if we have already imported (or attempted to import)
+ * the downloads database from the previous SQLite storage.
+ *
+ * @return boolean True if we the previous DB was imported.
+ */
+ get _importedFromSqlite() {
+ try {
+ return Services.prefs.getBoolPref(kPrefImportedFromSqlite);
+ } catch (ex) {
+ return false;
+ }
+ },
};
////////////////////////////////////////////////////////////////////////////////
diff --git a/toolkit/components/jsdownloads/src/Downloads.jsm b/toolkit/components/jsdownloads/src/Downloads.jsm
index a31c77669c20..eca0ec9702e8 100644
--- a/toolkit/components/jsdownloads/src/Downloads.jsm
+++ b/toolkit/components/jsdownloads/src/Downloads.jsm
@@ -148,7 +148,7 @@ this.Downloads = {
let list = new DownloadList(true);
try {
yield DownloadIntegration.addListObservers(list, false);
- yield DownloadIntegration.loadPersistent(list);
+ yield DownloadIntegration.initializePublicDownloadList(list);
} catch (ex) {
Cu.reportError(ex);
}
diff --git a/toolkit/components/jsdownloads/src/moz.build b/toolkit/components/jsdownloads/src/moz.build
index 1b24915e3fd2..48df46343f8a 100644
--- a/toolkit/components/jsdownloads/src/moz.build
+++ b/toolkit/components/jsdownloads/src/moz.build
@@ -11,6 +11,7 @@ EXTRA_COMPONENTS += [
EXTRA_JS_MODULES += [
'DownloadCore.jsm',
+ 'DownloadImport.jsm',
'DownloadList.jsm',
'DownloadStore.jsm',
'DownloadUIHelper.jsm',
From eb63e1dd94f0ac21ef7b6178a48e996ac30da788 Mon Sep 17 00:00:00 2001
From: Marco Castelluccio
Date: Fri, 16 Aug 2013 15:17:52 -0400
Subject: [PATCH 17/46] Bug 892837 - Support permissions in desktop webrt.
r=myk
--HG--
rename : webapprt/content/mochitest.js => webapprt/content/mochitest-shared.js
---
webapprt/CommandLineHandler.js | 16 ++---
webapprt/Startup.jsm | 90 +++++++++++++++++++++++-----
webapprt/WebappRT.jsm | 3 +-
webapprt/WebappsHandler.jsm | 16 ++---
webapprt/content/mochitest-shared.js | 90 ++++++++++++++++++++++++++++
webapprt/content/mochitest.js | 86 ++++++++------------------
webapprt/content/mochitest.xul | 28 ---------
webapprt/content/webapp.js | 13 ----
webapprt/jar.mn | 1 +
webapprt/prefs.js | 3 -
webapprt/test/chrome/head.js | 4 +-
11 files changed, 214 insertions(+), 136 deletions(-)
create mode 100644 webapprt/content/mochitest-shared.js
diff --git a/webapprt/CommandLineHandler.js b/webapprt/CommandLineHandler.js
index c0e43c24c5f8..66439ea62fa8 100644
--- a/webapprt/CommandLineHandler.js
+++ b/webapprt/CommandLineHandler.js
@@ -8,7 +8,6 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://webapprt/modules/WebappRT.jsm");
function CommandLineHandler() {}
@@ -30,12 +29,15 @@ CommandLineHandler.prototype = {
"chrome,dialog=no",
args);
} else {
- args.setProperty("url", WebappRT.launchURI);
- Services.ww.openWindow(null,
- "chrome://webapprt/content/webapp.xul",
- "_blank",
- "chrome,dialog=no,resizable,scrollbars,centerscreen",
- args);
+ // We're opening the window here in order to show it as soon as possible.
+ let window = Services.ww.openWindow(null,
+ "chrome://webapprt/content/webapp.xul",
+ "_blank",
+ "chrome,dialog=no,resizable,scrollbars,centerscreen",
+ null);
+ // Load the module to start up the app
+ Cu.import("resource://webapprt/modules/Startup.jsm");
+ startup(window);
}
},
diff --git a/webapprt/Startup.jsm b/webapprt/Startup.jsm
index 8f4f3bd2c67a..4980b68336b7 100644
--- a/webapprt/Startup.jsm
+++ b/webapprt/Startup.jsm
@@ -2,31 +2,93 @@
* 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 is imported by each new webapp window but is only evaluated the first
- * time it is imported. Put stuff here that you want to happen once on startup
- * before the webapp is loaded. But note that the "stuff" happens immediately
- * the first time this module is imported. So only put stuff here that must
- * happen before the webapp is loaded. */
+/* This module is imported at the startup of an application. It takes care of
+ * permissions installation, application url loading, security settings. Put
+ * stuff here that you want to happen once on startup before the webapp is
+ * loaded. */
-this.EXPORTED_SYMBOLS = [];
+this.EXPORTED_SYMBOLS = ["startup"];
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
-
// Initialize DOMApplicationRegistry by importing Webapps.jsm.
Cu.import("resource://gre/modules/Webapps.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/PermissionsInstaller.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
// Initialize window-independent handling of webapps- notifications.
Cu.import("resource://webapprt/modules/WebappsHandler.jsm");
-WebappsHandler.init();
+Cu.import("resource://webapprt/modules/WebappRT.jsm");
-// On firstrun, set permissions to their default values.
-if (!Services.prefs.getBoolPref("webapprt.firstrun")) {
- // Once we support packaged apps, set their permissions here on firstrun.
+function isFirstRunOrUpdate() {
+ let savedBuildID = null;
+ try {
+ savedBuildID = Services.prefs.getCharPref("webapprt.buildID");
+ } catch (e) {}
- // Now that we've set the appropriate permissions, twiddle the firstrun
- // flag so we don't try to do so again.
- Services.prefs.setBoolPref("webapprt.firstrun", true);
+ let ourBuildID = Services.appinfo.platformBuildID;
+
+ if (ourBuildID != savedBuildID) {
+ Services.prefs.setCharPref("webapprt.buildID", ourBuildID);
+ return true;
+ }
+
+ return false;
+}
+
+// Observes all the events needed to actually launch an application.
+// It waits for XUL window and webapps registry loading.
+this.startup = function(window) {
+ return Task.spawn(function () {
+ // Observe registry loading.
+ let deferredRegistry = Promise.defer();
+ function observeRegistryLoading() {
+ Services.obs.removeObserver(observeRegistryLoading, "webapps-registry-start");
+ deferredRegistry.resolve();
+ }
+ Services.obs.addObserver(observeRegistryLoading, "webapps-registry-start", false);
+
+ // Observe XUL window loading.
+ // For tests, it could be already loaded.
+ let deferredWindowLoad = Promise.defer();
+ if (window.document && window.document.getElementById("content")) {
+ deferredWindowLoad.resolve();
+ } else {
+ window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad, false);
+ deferredWindowLoad.resolve();
+ });
+ }
+
+ // Wait for webapps registry loading.
+ yield deferredRegistry.promise;
+
+ // Install/update permissions and get the appID from the webapps registry.
+ let appID = Ci.nsIScriptSecurityManager.NO_APP_ID;
+ let manifestURL = WebappRT.config.app.manifestURL;
+ if (manifestURL) {
+ appID = DOMApplicationRegistry.getAppLocalIdByManifestURL(manifestURL);
+
+ // On firstrun, set permissions to their default values.
+ // When the webapp runtime is updated, update the permissions.
+ // TODO: Update the permissions when the application is updated.
+ if (isFirstRunOrUpdate(Services.prefs)) {
+ PermissionsInstaller.installPermissions(WebappRT.config.app, true);
+ }
+ }
+
+ // Wait for XUL window loading
+ yield deferredWindowLoad.promise;
+
+ // Get the element in the webapp.xul window.
+ let appBrowser = window.document.getElementById("content");
+
+ // Set the principal to the correct appID and launch the application.
+ appBrowser.docShell.setIsApp(appID);
+ appBrowser.setAttribute("src", WebappRT.launchURI);
+ }).then(null, Cu.reportError.bind(Cu));
}
diff --git a/webapprt/WebappRT.jsm b/webapprt/WebappRT.jsm
index acd8f2017158..141ac4730838 100644
--- a/webapprt/WebappRT.jsm
+++ b/webapprt/WebappRT.jsm
@@ -24,14 +24,13 @@ this.WebappRT = {
if (this._config)
return this._config;
- let config;
let webappFile = FileUtils.getFile("AppRegD", ["webapp.json"]);
let inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(Ci.nsIFileInputStream);
inputStream.init(webappFile, -1, 0, 0);
let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
- config = json.decodeFromStream(inputStream, webappFile.fileSize);
+ let config = json.decodeFromStream(inputStream, webappFile.fileSize);
return this._config = config;
},
diff --git a/webapprt/WebappsHandler.jsm b/webapprt/WebappsHandler.jsm
index 888dae75c45b..b9d2c300383d 100644
--- a/webapprt/WebappsHandler.jsm
+++ b/webapprt/WebappsHandler.jsm
@@ -10,6 +10,7 @@ let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Webapps.jsm");
Cu.import("resource://gre/modules/AppsUtils.jsm");
@@ -17,12 +18,6 @@ Cu.import("resource://gre/modules/WebappsInstaller.jsm");
Cu.import("resource://gre/modules/WebappOSUtils.jsm");
this.WebappsHandler = {
- init: function() {
- Services.obs.addObserver(this, "webapps-ask-install", false);
- Services.obs.addObserver(this, "webapps-launch", false);
- Services.obs.addObserver(this, "webapps-uninstall", false);
- },
-
observe: function(subject, topic, data) {
data = JSON.parse(data);
data.mm = subject;
@@ -83,5 +78,12 @@ this.WebappsHandler = {
} else {
DOMApplicationRegistry.denyInstall(data);
}
- }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference])
};
+
+Services.obs.addObserver(WebappsHandler, "webapps-ask-install", false);
+Services.obs.addObserver(WebappsHandler, "webapps-launch", false);
+Services.obs.addObserver(WebappsHandler, "webapps-uninstall", false);
diff --git a/webapprt/content/mochitest-shared.js b/webapprt/content/mochitest-shared.js
new file mode 100644
index 000000000000..e4559c7324d5
--- /dev/null
+++ b/webapprt/content/mochitest-shared.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+/* Note: this script is loaded by both mochitest.js and head.js, so make sure
+ * the code you put here can be evaluated by both! */
+
+Cu.import("resource://webapprt/modules/WebappRT.jsm");
+
+// When WebappsHandler opens an install confirmation dialog for apps we install,
+// close it, which will be seen as the equivalent of cancelling the install.
+// This doesn't prevent us from installing those apps, as we listen for the same
+// notification as WebappsHandler and do the install ourselves. It just
+// prevents the modal installation confirmation dialogs from hanging tests.
+Services.ww.registerNotification({
+ observe: function(win, topic) {
+ if (topic == "domwindowopened") {
+ // Wait for load because the window is not yet sufficiently initialized.
+ win.addEventListener("load", function onLoadWindow() {
+ win.removeEventListener("load", onLoadWindow, false);
+ if (win.location == "chrome://global/content/commonDialog.xul" &&
+ win.opener == window) {
+ win.close();
+ }
+ }, false);
+ }
+ }
+});
+
+/**
+ * Transmogrify the runtime session into one for the given webapp.
+ *
+ * @param {String} manifestURL
+ * The URL of the webapp's manifest, relative to the base URL.
+ * Note that the base URL points to the *chrome* WebappRT mochitests,
+ * so you must supply an absolute URL to manifests elsewhere.
+ * @param {Object} parameters
+ * The value to pass as the "parameters" argument to
+ * mozIDOMApplicationRegistry.install, e.g., { receipts: ... }.
+ * Use undefined to pass nothing.
+ * @param {Function} onBecome
+ * The callback to call once the transmogrification is complete.
+ */
+function becomeWebapp(manifestURL, parameters, onBecome) {
+ function observeInstall(subj, topic, data) {
+ Services.obs.removeObserver(observeInstall, "webapps-ask-install");
+
+ // Step 2: Configure the runtime session to represent the app.
+ // We load DOMApplicationRegistry into a local scope to avoid appearing
+ // to leak it.
+
+ let scope = {};
+ Cu.import("resource://gre/modules/Webapps.jsm", scope);
+ Cu.import("resource://webapprt/modules/Startup.jsm", scope);
+ scope.DOMApplicationRegistry.confirmInstall(JSON.parse(data));
+
+ let installRecord = JSON.parse(data);
+ installRecord.mm = subj;
+ installRecord.registryDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
+ WebappRT.config = installRecord;
+
+ let win = Services.wm.getMostRecentWindow("webapprt:webapp");
+ if (!win) {
+ win = Services.ww.openWindow(null,
+ "chrome://webapprt/content/webapp.xul",
+ "_blank",
+ "chrome,dialog=no,resizable,scrollbars,centerscreen",
+ null);
+ }
+
+ let promise = scope.startup(win);
+
+ // During chrome tests, we use the same window to load all the tests. We
+ // need to change the buildID so that the permissions for the currently
+ // tested application get installed.
+ Services.prefs.setCharPref("webapprt.buildID", WebappRT.config.app.manifestURL);
+
+ // During tests, the webapps registry is already loaded.
+ // The Startup module needs to be notified when the webapps registry
+ // gets loaded, so we do that now.
+ Services.obs.notifyObservers(this, "webapps-registry-start", null);
+
+ promise.then(onBecome);
+ }
+ Services.obs.addObserver(observeInstall, "webapps-ask-install", false);
+
+ // Step 1: Install the app at the URL specified by the manifest.
+ let url = Services.io.newURI(manifestURL, null, null);
+ navigator.mozApps.install(url.spec, parameters);
+}
diff --git a/webapprt/content/mochitest.js b/webapprt/content/mochitest.js
index 064011dafb2d..7286e7676690 100644
--- a/webapprt/content/mochitest.js
+++ b/webapprt/content/mochitest.js
@@ -2,71 +2,37 @@
* 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/. */
-/* Note: this script is loaded by both mochitest.xul and head.js, so make sure
- * the code you put here can be evaluated by both! */
-
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://webapprt/modules/WebappRT.jsm");
-// When WebappsHandler opens an install confirmation dialog for apps we install,
-// close it, which will be seen as the equivalent of cancelling the install.
-// This doesn't prevent us from installing those apps, as we listen for the same
-// notification as WebappsHandler and do the install ourselves. It just
-// prevents the modal installation confirmation dialogs from hanging tests.
-Services.ww.registerNotification({
- observe: function(win, topic) {
- if (topic == "domwindowopened") {
- // Wait for load because the window is not yet sufficiently initialized.
- win.addEventListener("load", function onLoadWindow() {
- win.removeEventListener("load", onLoadWindow, false);
- if (win.location == "chrome://global/content/commonDialog.xul" &&
- win.opener == window) {
- win.close();
- }
- }, false);
+Services.scriptloader
+ .loadSubScript("chrome://webapprt/content/mochitest-shared.js", this);
+
+// In test mode, the runtime isn't configured until we tell it to become
+// an app, which requires us to use DOMApplicationRegistry to install one.
+// But DOMApplicationRegistry needs to know the location of its registry dir,
+// so we need to configure the runtime with at least that information.
+WebappRT.config = {
+ registryDir: Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+};
+
+
+Cu.import("resource://gre/modules/Webapps.jsm");
+
+DOMApplicationRegistry.allAppsLaunchable = true;
+
+becomeWebapp("http://mochi.test:8888/tests/webapprt/test/content/test.webapp",
+ undefined, function onBecome() {
+ if (window.arguments && window.arguments[0]) {
+ let testUrl = window.arguments[0].QueryInterface(Ci.nsIPropertyBag2).get("url");
+
+ if (testUrl) {
+ let win = Services.wm.getMostRecentWindow("webapprt:webapp");
+ win.document.getElementById("content").setAttribute("src", testUrl);
}
}
+
+ window.close();
});
-
-/**
- * Transmogrify the runtime session into one for the given webapp.
- *
- * @param {String} manifestURL
- * The URL of the webapp's manifest, relative to the base URL.
- * Note that the base URL points to the *chrome* WebappRT mochitests,
- * so you must supply an absolute URL to manifests elsewhere.
- * @param {Object} parameters
- * The value to pass as the "parameters" argument to
- * mozIDOMApplicationRegistry.install, e.g., { receipts: ... }.
- * Use undefined to pass nothing.
- * @param {Function} onBecome
- * The callback to call once the transmogrification is complete.
- */
-function becomeWebapp(manifestURL, parameters, onBecome) {
- function observeInstall(subj, topic, data) {
- Services.obs.removeObserver(observeInstall, "webapps-ask-install");
-
- // Step 2: Configure the runtime session to represent the app.
- // We load DOMApplicationRegistry into a local scope to avoid appearing
- // to leak it.
-
- let scope = {};
- Cu.import("resource://gre/modules/Webapps.jsm", scope);
- scope.DOMApplicationRegistry.confirmInstall(JSON.parse(data));
-
- let installRecord = JSON.parse(data);
- installRecord.mm = subj;
- installRecord.registryDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
- WebappRT.config = installRecord;
-
- onBecome();
- }
- Services.obs.addObserver(observeInstall, "webapps-ask-install", false);
-
- // Step 1: Install the app at the URL specified by the manifest.
- let url = Services.io.newURI(manifestURL, null, null);
- navigator.mozApps.install(url.spec, parameters);
-}
diff --git a/webapprt/content/mochitest.xul b/webapprt/content/mochitest.xul
index c30408d66b25..1c5a912482e8 100644
--- a/webapprt/content/mochitest.xul
+++ b/webapprt/content/mochitest.xul
@@ -11,34 +11,6 @@
-
-
diff --git a/webapprt/content/webapp.js b/webapprt/content/webapp.js
index 90c05bf57631..f432137accd4 100644
--- a/webapprt/content/webapp.js
+++ b/webapprt/content/webapp.js
@@ -6,7 +6,6 @@ const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
-Cu.import("resource://webapprt/modules/Startup.jsm");
Cu.import("resource://webapprt/modules/WebappRT.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -49,10 +48,6 @@ let progressListener = {
function onLoad() {
window.removeEventListener("load", onLoad, false);
- let args = window.arguments && window.arguments[0] ?
- window.arguments[0].QueryInterface(Ci.nsIPropertyBag2) :
- null;
-
gAppBrowser.addProgressListener(progressListener,
Ci.nsIWebProgress.NOTIFY_LOCATION |
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
@@ -63,14 +58,6 @@ function onLoad() {
// This doesn't capture clicks so content can capture them itself and do
// something different if it doesn't want the default behavior.
gAppBrowser.addEventListener("click", onContentClick, false, true);
-
- // This is not the only way that a URL gets loaded in the app browser.
- // When content calls openWindow(), there are no window.arguments,
- // but something in the platform loads the URL specified by the content.
- if (args && args.hasKey("url")) {
- gAppBrowser.setAttribute("src", args.get("url"));
- }
-
}
window.addEventListener("load", onLoad, false);
diff --git a/webapprt/jar.mn b/webapprt/jar.mn
index 75b76ae13203..0c33e9b4c9dd 100644
--- a/webapprt/jar.mn
+++ b/webapprt/jar.mn
@@ -6,5 +6,6 @@ webapprt.jar:
% content webapprt %content/
* content/webapp.js (content/webapp.js)
* content/webapp.xul (content/webapp.xul)
+ content/mochitest-shared.js (content/mochitest-shared.js)
content/mochitest.js (content/mochitest.js)
content/mochitest.xul (content/mochitest.xul)
diff --git a/webapprt/prefs.js b/webapprt/prefs.js
index 9445bc792f30..a9812693a8e9 100644
--- a/webapprt/prefs.js
+++ b/webapprt/prefs.js
@@ -16,9 +16,6 @@ pref("extensions.installDistroAddons", false);
// Disable the add-on compatibility dialog
pref("extensions.showMismatchUI", false);
-// Whether or not we've ever run. We use this to set permissions on firstrun.
-pref("webapprt.firstrun", false);
-
// Set reportURL for crashes
pref("breakpad.reportURL", "https://crash-stats.mozilla.com/report/index/");
diff --git a/webapprt/test/chrome/head.js b/webapprt/test/chrome/head.js
index bd2107f066c6..e91af6ba2fa9 100644
--- a/webapprt/test/chrome/head.js
+++ b/webapprt/test/chrome/head.js
@@ -6,8 +6,8 @@ Cu.import("resource://gre/modules/Services.jsm");
// Some of the code we want to provide to chrome mochitests is in another file
// so we can share it with the mochitest shim window, thus we need to load it.
-Services.scriptloader.loadSubScript("chrome://webapprt/content/mochitest.js",
- this);
+Services.scriptloader
+ .loadSubScript("chrome://webapprt/content/mochitest-shared.js", this);
const MANIFEST_URL_BASE = Services.io.newURI(
"http://test/webapprtChrome/webapprt/test/chrome/", null, null);
From 75b395e439ae08d1576319e6f382dea7871915e0 Mon Sep 17 00:00:00 2001
From: Marco Castelluccio
Date: Fri, 16 Aug 2013 15:17:52 -0400
Subject: [PATCH 18/46] Bug 892837 - Tests for desktop webrt permissions. r=myk
---
webapprt/test/chrome/Makefile.in | 8 +++++
webapprt/test/chrome/browser_noperm.js | 31 +++++++++++++++++
webapprt/test/chrome/browser_webperm.js | 36 ++++++++++++++++++++
webapprt/test/chrome/noperm.html | 9 +++++
webapprt/test/chrome/noperm.webapp | 5 +++
webapprt/test/chrome/noperm.webapp^headers^ | 1 +
webapprt/test/chrome/webperm.html | 9 +++++
webapprt/test/chrome/webperm.webapp | 15 ++++++++
webapprt/test/chrome/webperm.webapp^headers^ | 1 +
9 files changed, 115 insertions(+)
create mode 100644 webapprt/test/chrome/browser_noperm.js
create mode 100644 webapprt/test/chrome/browser_webperm.js
create mode 100644 webapprt/test/chrome/noperm.html
create mode 100644 webapprt/test/chrome/noperm.webapp
create mode 100644 webapprt/test/chrome/noperm.webapp^headers^
create mode 100644 webapprt/test/chrome/webperm.html
create mode 100644 webapprt/test/chrome/webperm.webapp
create mode 100644 webapprt/test/chrome/webperm.webapp^headers^
diff --git a/webapprt/test/chrome/Makefile.in b/webapprt/test/chrome/Makefile.in
index a9898b3ae07d..818e5792fa40 100644
--- a/webapprt/test/chrome/Makefile.in
+++ b/webapprt/test/chrome/Makefile.in
@@ -20,6 +20,14 @@ MOCHITEST_WEBAPPRT_CHROME_FILES = \
window-title.webapp \
window-title.webapp^headers^ \
window-title.html \
+ browser_webperm.js \
+ webperm.webapp \
+ webperm.webapp^headers^ \
+ webperm.html \
+ browser_noperm.js \
+ noperm.webapp \
+ noperm.webapp^headers^ \
+ noperm.html \
$(NULL)
include $(topsrcdir)/config/rules.mk
diff --git a/webapprt/test/chrome/browser_noperm.js b/webapprt/test/chrome/browser_noperm.js
new file mode 100644
index 000000000000..240cf1d81af7
--- /dev/null
+++ b/webapprt/test/chrome/browser_noperm.js
@@ -0,0 +1,31 @@
+Cu.import("resource://webapprt/modules/WebappRT.jsm");
+let { AllPossiblePermissions } =
+ Cu.import("resource://gre/modules/PermissionsInstaller.jsm", {});
+let { AppsUtils } = Cu.import("resource://gre/modules/AppsUtils.jsm", {});
+let { DOMApplicationRegistry } =
+ Cu.import("resource://gre/modules/Webapps.jsm", {});
+
+function test() {
+ waitForExplicitFinish();
+
+ loadWebapp("noperm.webapp", undefined, function onLoad() {
+ let app = WebappRT.config.app;
+
+ // Check that the app is non privileged.
+ is(AppsUtils.getAppManifestStatus(app.manifest), Ci.nsIPrincipal.APP_STATUS_INSTALLED, "The app is not privileged");
+
+ // Check that the app principal has the correct appId.
+ let principal = document.getElementById("content").contentDocument.defaultView.document.nodePrincipal;
+ is(DOMApplicationRegistry.getAppLocalIdByManifestURL(app.manifestURL), principal.appId, "Principal app ID correct");
+
+ // Check if all the permissions of the app are unknown.
+ for (let permName of AllPossiblePermissions) {
+ // Get the value for the permission.
+ let permValue = Services.perms.testExactPermissionFromPrincipal(principal, permName);
+
+ is(permValue, Ci.nsIPermissionManager.UNKNOWN_ACTION, "Permission " + permName + " unknown.");
+ }
+
+ finish();
+ });
+}
diff --git a/webapprt/test/chrome/browser_webperm.js b/webapprt/test/chrome/browser_webperm.js
new file mode 100644
index 000000000000..bd76ce5aae46
--- /dev/null
+++ b/webapprt/test/chrome/browser_webperm.js
@@ -0,0 +1,36 @@
+Cu.import("resource://webapprt/modules/WebappRT.jsm");
+let { AppsUtils } = Cu.import("resource://gre/modules/AppsUtils.jsm", {});
+let { DOMApplicationRegistry } =
+ Cu.import("resource://gre/modules/Webapps.jsm", {});
+let { PermissionsTable, PermissionsReverseTable } =
+ Cu.import("resource://gre/modules/PermissionsTable.jsm", {});
+
+function test() {
+ waitForExplicitFinish();
+
+ loadWebapp("webperm.webapp", undefined, function onLoad() {
+ let app = WebappRT.config.app;
+
+ // Check that the app is non privileged.
+ is(AppsUtils.getAppManifestStatus(app.manifest), Ci.nsIPrincipal.APP_STATUS_INSTALLED, "The app is not privileged");
+
+ // Check that the app principal has the correct appId.
+ let principal = document.getElementById("content").contentDocument.defaultView.document.nodePrincipal;
+ is(DOMApplicationRegistry.getAppLocalIdByManifestURL(app.manifestURL), principal.appId, "Principal app ID correct");
+
+ let perms = [ "indexedDB-unlimited", "offline-app", "pin-app", "geolocation",
+ "camera", "alarms", "tcp-socket", "network-events",
+ "webapps-manage", "desktop-notification" ];
+
+ for (let permName of perms) {
+ // Get the values for all the permission.
+ let permValue = Services.perms.testExactPermissionFromPrincipal(principal, permName);
+
+ // Check if the app has the permission as specified in the PermissionsTable.jsm file.
+ let realPerm = PermissionsReverseTable[permName];
+ is(permValue, PermissionsTable[realPerm]["app"], "Permission " + permName + " correctly set.");
+ }
+
+ finish();
+ });
+}
diff --git a/webapprt/test/chrome/noperm.html b/webapprt/test/chrome/noperm.html
new file mode 100644
index 000000000000..3472236aa982
--- /dev/null
+++ b/webapprt/test/chrome/noperm.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
This is the test webapp.
+
+
diff --git a/webapprt/test/chrome/noperm.webapp b/webapprt/test/chrome/noperm.webapp
new file mode 100644
index 000000000000..5fa4a18703da
--- /dev/null
+++ b/webapprt/test/chrome/noperm.webapp
@@ -0,0 +1,5 @@
+{
+ "name": "No Permissions Test Webapp",
+ "description": "A webapp for testing permission installation.",
+ "launch_path": "/webapprtChrome/webapprt/test/chrome/noperm.html"
+}
diff --git a/webapprt/test/chrome/noperm.webapp^headers^ b/webapprt/test/chrome/noperm.webapp^headers^
new file mode 100644
index 000000000000..a2367b11c78b
--- /dev/null
+++ b/webapprt/test/chrome/noperm.webapp^headers^
@@ -0,0 +1 @@
+Content-Type: application/x-web-app-manifest+json
diff --git a/webapprt/test/chrome/webperm.html b/webapprt/test/chrome/webperm.html
new file mode 100644
index 000000000000..3472236aa982
--- /dev/null
+++ b/webapprt/test/chrome/webperm.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
This is the test webapp.
+
+
diff --git a/webapprt/test/chrome/webperm.webapp b/webapprt/test/chrome/webperm.webapp
new file mode 100644
index 000000000000..81f086167709
--- /dev/null
+++ b/webapprt/test/chrome/webperm.webapp
@@ -0,0 +1,15 @@
+{
+ "name": "Hosted Permissions Test Webapp",
+ "description": "A webapp for testing permission installation.",
+ "launch_path": "/webapprtChrome/webapprt/test/chrome/webperm.html",
+ "permissions": {
+ "storage": { "description": "I need to store 1 million dollars in your bank account" },
+ "geolocation": { "description": "Desc" },
+ "camera": { "description": "Desc" },
+ "alarms": { "description": "Desc" },
+ "tcp-socket": { "description": "Desc" },
+ "network-events": { "description": "Desc" },
+ "webapps-manage": { "description": "Desc" },
+ "desktop-notification": { "description": "Desc" }
+ }
+}
diff --git a/webapprt/test/chrome/webperm.webapp^headers^ b/webapprt/test/chrome/webperm.webapp^headers^
new file mode 100644
index 000000000000..a2367b11c78b
--- /dev/null
+++ b/webapprt/test/chrome/webperm.webapp^headers^
@@ -0,0 +1 @@
+Content-Type: application/x-web-app-manifest+json
From 01020a4a2560a0d71e11cdefbd4ef86397e435e7 Mon Sep 17 00:00:00 2001
From: Marco Castelluccio
Date: Fri, 16 Aug 2013 15:17:53 -0400
Subject: [PATCH 19/46] Bug 892837 - Add manifestURL to the webapp.json file.
r=myk
---
toolkit/webapps/WebappsInstaller.jsm | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/toolkit/webapps/WebappsInstaller.jsm b/toolkit/webapps/WebappsInstaller.jsm
index 066d2a01d95b..43d5cacb105a 100644
--- a/toolkit/webapps/WebappsInstaller.jsm
+++ b/toolkit/webapps/WebappsInstaller.jsm
@@ -178,7 +178,8 @@ NativeApp.prototype = {
"registryDir": registryFolder.path,
"app": {
"manifest": aManifest,
- "origin": aData.app.origin
+ "origin": aData.app.origin,
+ "manifestURL": aData.app.manifestURL
}
};
From 70f1b9a2e8c6704b3b1356433a73f92ecc7c98df Mon Sep 17 00:00:00 2001
From: Robert Bindar
Date: Fri, 16 Aug 2013 15:17:53 -0400
Subject: [PATCH 20/46] Bug 903404 - Move toolkit/content/aboutNetworking.net
to toolkit/themes/shared. r=gavin
--HG--
rename : toolkit/content/aboutNetworking.css => toolkit/themes/shared/aboutNetworking.css
---
toolkit/content/aboutNetworking.xhtml | 2 +-
toolkit/content/jar.mn | 1 -
toolkit/themes/osx/mozapps/jar.mn | 1 +
toolkit/{content => themes/shared}/aboutNetworking.css | 0
toolkit/themes/windows/mozapps/jar.mn | 2 ++
5 files changed, 4 insertions(+), 2 deletions(-)
rename toolkit/{content => themes/shared}/aboutNetworking.css (100%)
diff --git a/toolkit/content/aboutNetworking.xhtml b/toolkit/content/aboutNetworking.xhtml
index d1c0203aeda9..0674a4b6e588 100644
--- a/toolkit/content/aboutNetworking.xhtml
+++ b/toolkit/content/aboutNetworking.xhtml
@@ -15,7 +15,7 @@
&aboutNetworking.title;
-
+
diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn
index 5c011ed6ac7c..44f07e749abd 100644
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -16,7 +16,6 @@ toolkit.jar:
content/global/aboutRights-unbranded.xhtml (aboutRights-unbranded.xhtml)
content/global/aboutNetworking.js
content/global/aboutNetworking.xhtml
- content/global/aboutNetworking.css
* content/global/aboutSupport.js
* content/global/aboutSupport.xhtml
* content/global/aboutTelemetry.js
diff --git a/toolkit/themes/osx/mozapps/jar.mn b/toolkit/themes/osx/mozapps/jar.mn
index fef0042f7f72..425b45715524 100644
--- a/toolkit/themes/osx/mozapps/jar.mn
+++ b/toolkit/themes/osx/mozapps/jar.mn
@@ -56,6 +56,7 @@ toolkit.jar:
skin/classic/mozapps/passwordmgr/key-16@2x.png (passwordmgr/key-16@2x.png)
skin/classic/mozapps/passwordmgr/key-64.png (passwordmgr/key-64.png)
skin/classic/mozapps/plugins/pluginProblem.css (../../shared/plugins/pluginProblem.css)
+ skin/classic/mozapps/aboutNetworking.css (../../shared/aboutNetworking.css)
skin/classic/mozapps/plugins/contentPluginActivate.png (../../shared/plugins/contentPluginActivate.png)
skin/classic/mozapps/plugins/contentPluginBlocked.png (../../shared/plugins/contentPluginBlocked.png)
skin/classic/mozapps/plugins/contentPluginClose.png (../../shared/plugins/contentPluginClose.png)
diff --git a/toolkit/content/aboutNetworking.css b/toolkit/themes/shared/aboutNetworking.css
similarity index 100%
rename from toolkit/content/aboutNetworking.css
rename to toolkit/themes/shared/aboutNetworking.css
diff --git a/toolkit/themes/windows/mozapps/jar.mn b/toolkit/themes/windows/mozapps/jar.mn
index 3b7512d21fda..286ea6d52a13 100644
--- a/toolkit/themes/windows/mozapps/jar.mn
+++ b/toolkit/themes/windows/mozapps/jar.mn
@@ -60,6 +60,7 @@ toolkit.jar:
skin/classic/mozapps/places/defaultFavicon.png (places/defaultFavicon.png)
#endif
skin/classic/mozapps/plugins/pluginProblem.css (../../shared/plugins/pluginProblem.css)
+ skin/classic/mozapps/aboutNetworking.css (../../shared/aboutNetworking.css)
skin/classic/mozapps/plugins/contentPluginActivate.png (../../shared/plugins/contentPluginActivate.png)
skin/classic/mozapps/plugins/contentPluginBlocked.png (../../shared/plugins/contentPluginBlocked.png)
skin/classic/mozapps/plugins/contentPluginClose.png (../../shared/plugins/contentPluginClose.png)
@@ -140,6 +141,7 @@ toolkit.jar:
skin/classic/aero/mozapps/places/defaultFavicon.png (places/defaultFavicon.png)
#endif
skin/classic/aero/mozapps/plugins/pluginProblem.css (../../shared/plugins/pluginProblem.css)
+ skin/classic/aero/mozapps/aboutNetworking.css (../../shared/aboutNetworking.css)
skin/classic/aero/mozapps/plugins/contentPluginActivate.png (../../shared/plugins/contentPluginActivate.png)
skin/classic/aero/mozapps/plugins/contentPluginBlocked.png (../../shared/plugins/contentPluginBlocked.png)
skin/classic/aero/mozapps/plugins/contentPluginClose.png (../../shared/plugins/contentPluginClose.png)
From 73620d69d7eca70cac9651b2c84a72efcff05c1d Mon Sep 17 00:00:00 2001
From: Paolo Amadini
Date: Fri, 16 Aug 2013 21:48:01 +0200
Subject: [PATCH 21/46] Bug 896927 - Handle the executable warning prompt.
r=enn
---
.../downloads/src/DownloadsCommon.jsm | 151 +++++-------------
.../jsdownloads/src/DownloadIntegration.jsm | 27 +++-
.../jsdownloads/src/DownloadUIHelper.jsm | 144 +++++++++++++++++
3 files changed, 214 insertions(+), 108 deletions(-)
diff --git a/browser/components/downloads/src/DownloadsCommon.jsm b/browser/components/downloads/src/DownloadsCommon.jsm
index e9722019a9ae..98f4d1fa7d10 100644
--- a/browser/components/downloads/src/DownloadsCommon.jsm
+++ b/browser/components/downloads/src/DownloadsCommon.jsm
@@ -53,6 +53,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
+ "resource://gre/modules/DownloadUIHelper.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
"resource://gre/modules/DownloadUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
@@ -68,27 +70,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
const nsIDM = Ci.nsIDownloadManager;
-const kDownloadsStringBundleUrl =
- "chrome://browser/locale/downloads/downloads.properties";
-
const kPrefBdmScanWhenDone = "browser.download.manager.scanWhenDone";
const kPrefBdmAlertOnExeOpen = "browser.download.manager.alertOnEXEOpen";
-const kDownloadsStringsRequiringFormatting = {
- sizeWithUnits: true,
- shortTimeLeftSeconds: true,
- shortTimeLeftMinutes: true,
- shortTimeLeftHours: true,
- shortTimeLeftDays: true,
- statusSeparator: true,
- statusSeparatorBeforeNumber: true,
- fileExecutableSecurityWarning: true
-};
-
-const kDownloadsStringsRequiringPluralForm = {
- otherDownloads2: true
-};
-
XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
return Components.Constructor("@mozilla.org/file/local;1",
"nsILocalFile", "initWithPath");
@@ -163,41 +147,6 @@ this.DownloadsCommon = {
}
this.error.apply(this, aMessageArgs);
},
- /**
- * Returns an object whose keys are the string names from the downloads string
- * bundle, and whose values are either the translated strings or functions
- * returning formatted strings.
- */
- get strings()
- {
- let strings = {};
- let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
- let enumerator = sb.getSimpleEnumeration();
- while (enumerator.hasMoreElements()) {
- let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
- let stringName = string.key;
- if (stringName in kDownloadsStringsRequiringFormatting) {
- strings[stringName] = function () {
- // Convert "arguments" to a real array before calling into XPCOM.
- return sb.formatStringFromName(stringName,
- Array.slice(arguments, 0),
- arguments.length);
- };
- } else if (stringName in kDownloadsStringsRequiringPluralForm) {
- strings[stringName] = function (aCount) {
- // Convert "arguments" to a real array before calling into XPCOM.
- let formattedString = sb.formatStringFromName(stringName,
- Array.slice(arguments, 0),
- arguments.length);
- return PluralForm.get(aCount, formattedString);
- };
- } else {
- strings[stringName] = string.value;
- }
- }
- delete this.strings;
- return this.strings = strings;
- },
/**
* Generates a very short string representing the given time left.
@@ -480,65 +429,44 @@ this.DownloadsCommon = {
if (!(aOwnerWindow instanceof Ci.nsIDOMWindow))
throw new Error("aOwnerWindow must be a dom-window object");
- // Confirm opening executable files if required.
+ let promiseShouldLaunch;
if (aFile.isExecutable()) {
- let showAlert = true;
- try {
- showAlert = Services.prefs.getBoolPref(kPrefBdmAlertOnExeOpen);
- } catch (ex) { }
-
- // On Vista and above, we rely on native security prompting for
- // downloaded content unless it's disabled.
- if (DownloadsCommon.isWinVistaOrHigher) {
- try {
- if (Services.prefs.getBoolPref(kPrefBdmScanWhenDone)) {
- showAlert = false;
- }
- } catch (ex) { }
- }
-
- if (showAlert) {
- let name = aFile.leafName;
- let message =
- DownloadsCommon.strings.fileExecutableSecurityWarning(name, name);
- let title =
- DownloadsCommon.strings.fileExecutableSecurityWarningTitle;
- let dontAsk =
- DownloadsCommon.strings.fileExecutableSecurityWarningDontAsk;
-
- let checkbox = { value: false };
- let open = Services.prompt.confirmCheck(aOwnerWindow, title, message,
- dontAsk, checkbox);
- if (!open) {
- return;
- }
-
- Services.prefs.setBoolPref(kPrefBdmAlertOnExeOpen,
- !checkbox.value);
- }
+ // We get a prompter for the provided window here, even though anchoring
+ // to the most recently active window should work as well.
+ promiseShouldLaunch =
+ DownloadUIHelper.getPrompter(aOwnerWindow)
+ .confirmLaunchExecutable(aFile.path);
+ } else {
+ promiseShouldLaunch = Promise.resolve(true);
}
- // Actually open the file.
- try {
- if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
- aMimeInfo.launchWithFile(aFile);
+ promiseShouldLaunch.then(shouldLaunch => {
+ if (!shouldLaunch) {
return;
}
- }
- catch(ex) { }
-
- // If either we don't have the mime info, or the preferred action failed,
- // attempt to launch the file directly.
- try {
- aFile.launch();
- }
- catch(ex) {
- // If launch fails, try sending it through the system's external "file:"
- // URL handler.
- Cc["@mozilla.org/uriloader/external-protocol-service;1"]
- .getService(Ci.nsIExternalProtocolService)
- .loadUrl(NetUtil.newURI(aFile));
- }
+
+ // Actually open the file.
+ try {
+ if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
+ aMimeInfo.launchWithFile(aFile);
+ return;
+ }
+ }
+ catch(ex) { }
+
+ // If either we don't have the mime info, or the preferred action failed,
+ // attempt to launch the file directly.
+ try {
+ aFile.launch();
+ }
+ catch(ex) {
+ // If launch fails, try sending it through the system's external "file:"
+ // URL handler.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadUrl(NetUtil.newURI(aFile));
+ }
+ }).then(null, Cu.reportError);
},
/**
@@ -574,6 +502,15 @@ this.DownloadsCommon = {
}
};
+/**
+ * Returns an object whose keys are the string names from the downloads string
+ * bundle, and whose values are either the translated strings or functions
+ * returning formatted strings.
+ */
+XPCOMUtils.defineLazyGetter(DownloadsCommon, "strings", function () {
+ return DownloadUIHelper.strings;
+});
+
/**
* Returns true if we are executing on Windows Vista or a later version.
*/
diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
index abf660de8c3e..9885753883a8 100644
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -29,6 +29,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
"resource://gre/modules/DownloadStore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
"resource://gre/modules/DownloadImport.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
+ "resource://gre/modules/DownloadUIHelper.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
@@ -61,6 +63,7 @@ XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
return null;
});
+// This will be replaced by "DownloadUIHelper.strings" (see bug 905123).
XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
return Services.strings.
createBundle("chrome://mozapps/locale/downloads/downloads.properties");
@@ -414,6 +417,24 @@ this.DownloadIntegration = {
let deferred = Task.spawn(function DI_launchDownload_task() {
let file = new FileUtils.File(aDownload.target.path);
+ // Ask for confirmation if the file is executable. We do this here,
+ // instead of letting the caller handle the prompt separately in the user
+ // interface layer, for two reasons. The first is because of its security
+ // nature, so that add-ons cannot forget to do this check. The second is
+ // that the system-level security prompt, if enabled, would be displayed
+ // at launch time in any case.
+ if (file.isExecutable() && !this.dontOpenFileAndFolder) {
+ // We don't anchor the prompt to a specific window intentionally, not
+ // only because this is the same behavior as the system-level prompt,
+ // but also because the most recently active window is the right choice
+ // in basically all cases.
+ let shouldLaunch = yield DownloadUIHelper.getPrompter()
+ .confirmLaunchExecutable(file.path);
+ if (!shouldLaunch) {
+ return;
+ }
+ }
+
// In case of a double extension, like ".tar.gz", we only
// consider the last one, because the MIME service cannot
// handle multiple extensions.
@@ -560,7 +581,11 @@ this.DownloadIntegration = {
*/
_createDownloadsDirectory: function DI_createDownloadsDirectory(aName) {
let directory = this._getDirectory(aName);
- directory.append(gStringBundle.GetStringFromName("downloadsFolder"));
+
+ // We read the name of the directory from the list of translated strings
+ // that is kept by the UI helper module, even if this string is not strictly
+ // displayed in the user interface.
+ directory.append(DownloadUIHelper.strings.downloadsFolder);
// Create the Downloads folder and ignore if it already exists.
return OS.File.makeDir(directory.path, { ignoreExisting: true }).
diff --git a/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
index 297abe0dcfeb..8013c9d476e7 100644
--- a/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
@@ -24,6 +24,31 @@ const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/commonjs/sdk/core/promise.js");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+const kStringBundleUrl =
+ "chrome://browser/locale/downloads/downloads.properties";
+
+const kStringsRequiringFormatting = {
+ sizeWithUnits: true,
+ shortTimeLeftSeconds: true,
+ shortTimeLeftMinutes: true,
+ shortTimeLeftHours: true,
+ shortTimeLeftDays: true,
+ statusSeparator: true,
+ statusSeparatorBeforeNumber: true,
+ fileExecutableSecurityWarning: true,
+};
+
+const kStringsRequiringPluralForm = {
+ otherDownloads2: true,
+};
+
////////////////////////////////////////////////////////////////////////////////
//// DownloadUIHelper
@@ -31,4 +56,123 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
* Provides functions to handle status and messages in the user interface.
*/
this.DownloadUIHelper = {
+ /**
+ * Returns an object that can be used to display prompts related to downloads.
+ *
+ * The prompts may be either anchored to a specified window, or anchored to
+ * the most recently active window, for example if the prompt is displayed in
+ * response to global notifications that are not associated with any window.
+ *
+ * @param aParent
+ * If specified, should reference the nsIDOMWindow to which the prompts
+ * should be attached. If omitted, the prompts will be attached to the
+ * most recently active window.
+ *
+ * @return A DownloadPrompter object.
+ */
+ getPrompter: function (aParent)
+ {
+ return new DownloadPrompter(aParent || null);
+ },
+};
+
+/**
+ * Returns an object whose keys are the string names from the downloads string
+ * bundle, and whose values are either the translated strings or functions
+ * returning formatted strings.
+ */
+XPCOMUtils.defineLazyGetter(DownloadUIHelper, "strings", function () {
+ let strings = {};
+ let sb = Services.strings.createBundle(kStringBundleUrl);
+ let enumerator = sb.getSimpleEnumeration();
+ while (enumerator.hasMoreElements()) {
+ let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
+ let stringName = string.key;
+ if (stringName in kStringsRequiringFormatting) {
+ strings[stringName] = function () {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ return sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ };
+ } else if (stringName in kStringsRequiringPluralForm) {
+ strings[stringName] = function (aCount) {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ let formattedString = sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ return PluralForm.get(aCount, formattedString);
+ };
+ } else {
+ strings[stringName] = string.value;
+ }
+ }
+ return strings;
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadPrompter
+
+/**
+ * Allows displaying prompts related to downloads.
+ *
+ * @param aParent
+ * The nsIDOMWindow to which prompts should be attached, or null to
+ * attach prompts to the most recently active window.
+ */
+function DownloadPrompter(aParent)
+{
+ this._prompter = Services.ww.getNewPrompter(aParent);
+}
+
+DownloadPrompter.prototype = {
+ /**
+ * nsIPrompt instance for displaying messages.
+ */
+ _prompter: null,
+
+ /**
+ * Displays a warning message box that informs that the specified file is
+ * executable, and asks whether the user wants to launch it. The user is
+ * given the option of disabling future instances of this warning.
+ *
+ * @param aPath
+ * String containing the full path to the file to be opened.
+ *
+ * @return {Promise}
+ * @resolves Boolean indicating whether the launch operation can continue.
+ * @rejects JavaScript exception.
+ */
+ confirmLaunchExecutable: function (aPath)
+ {
+ const kPrefAlertOnEXEOpen = "browser.download.manager.alertOnEXEOpen";
+
+ try {
+ try {
+ if (!Services.prefs.getBoolPref(kPrefAlertOnEXEOpen)) {
+ return Promise.resolve(true);
+ }
+ } catch (ex) {
+ // If the preference does not exist, continue with the prompt.
+ }
+
+ let leafName = OS.Path.basename(aPath);
+
+ let s = DownloadUIHelper.strings;
+ let checkState = { value: false };
+ let shouldLaunch = this._prompter.confirmCheck(
+ s.fileExecutableSecurityWarningTitle,
+ s.fileExecutableSecurityWarning(leafName, leafName),
+ s.fileExecutableSecurityWarningDontAsk,
+ checkState);
+
+ if (shouldLaunch) {
+ Services.prefs.setBoolPref(kPrefAlertOnEXEOpen, !checkState.value);
+ }
+
+ return Promise.resolve(shouldLaunch);
+ } catch (ex) {
+ return Promise.reject(ex);
+ }
+ },
};
From e3f4881a61eb213e1a0b3f8c494133d037348839 Mon Sep 17 00:00:00 2001
From: Jared Wein
Date: Fri, 16 Aug 2013 16:31:35 -0400
Subject: [PATCH 22/46] Backed out changeset 707307cc53a6 (bug 897160)
---
browser/base/content/browser.css | 8 --------
1 file changed, 8 deletions(-)
diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
index d76cfb125464..1557205137bd 100644
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -5,14 +5,6 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
-#main-window:not([chromehidden~="toolbar"]) {
-%ifdef XP_MACOSX
- min-width: 425px;
-%else
- min-width: 390px;
-%endif
-}
-
searchbar {
-moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
}
From 4d820ba7572af8217614c744f6a74c371c8bbe78 Mon Sep 17 00:00:00 2001
From: Mark Capella
Date: Fri, 16 Aug 2013 16:45:42 -0400
Subject: [PATCH 23/46] Bug 864589 - Show/hide text selection handles if a
selection is programatically added/removed, r=margaret, ehsan
---
content/base/public/nsISelectionListener.idl | 2 ++
layout/generic/nsSelection.cpp | 8 ++++++++
mobile/android/chrome/content/SelectionHandler.js | 13 +++++++++++++
3 files changed, 23 insertions(+)
diff --git a/content/base/public/nsISelectionListener.idl b/content/base/public/nsISelectionListener.idl
index 9e642ee6a907..3179f141e502 100644
--- a/content/base/public/nsISelectionListener.idl
+++ b/content/base/public/nsISelectionListener.idl
@@ -17,6 +17,8 @@ interface nsISelectionListener : nsISupports
const short MOUSEUP_REASON=4;/*bitflags*/
const short KEYPRESS_REASON=8;/*bitflags*/
const short SELECTALL_REASON=16;
+ const short COLLAPSETOSTART_REASON=32;
+ const short COLLAPSETOEND_REASON=64;
void notifySelectionChanged(in nsIDOMDocument doc, in nsISelection sel, in short reason);
};
diff --git a/layout/generic/nsSelection.cpp b/layout/generic/nsSelection.cpp
index 28fe8ff181b0..c0f03da5249d 100644
--- a/layout/generic/nsSelection.cpp
+++ b/layout/generic/nsSelection.cpp
@@ -4444,6 +4444,10 @@ Selection::CollapseToStart()
if (!firstRange)
return NS_ERROR_FAILURE;
+ if (mFrameSelection) {
+ int16_t reason = mFrameSelection->PopReason() | nsISelectionListener::COLLAPSETOSTART_REASON;
+ mFrameSelection->PostReason(reason);
+ }
return Collapse(firstRange->GetStartParent(), firstRange->StartOffset());
}
@@ -4464,6 +4468,10 @@ Selection::CollapseToEnd()
if (!lastRange)
return NS_ERROR_FAILURE;
+ if (mFrameSelection) {
+ int16_t reason = mFrameSelection->PopReason() | nsISelectionListener::COLLAPSETOEND_REASON;
+ mFrameSelection->PostReason(reason);
+ }
return Collapse(lastRange->GetEndParent(), lastRange->EndOffset());
}
diff --git a/mobile/android/chrome/content/SelectionHandler.js b/mobile/android/chrome/content/SelectionHandler.js
index 7f80c1f9efb2..34be7e3546ee 100644
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -174,6 +174,14 @@ var SelectionHandler = {
};
},
+ notifySelectionChanged: function sh_notifySelectionChanged(aDocument, aSelection, aReason) {
+ // If the selection was collapsed to Start or to End, always close it
+ if ((aReason & Ci.nsISelectionListener.COLLAPSETOSTART_REASON) ||
+ (aReason & Ci.nsISelectionListener.COLLAPSETOEND_REASON)) {
+ this._closeSelection();
+ }
+ },
+
/*
* Called from browser.js when the user long taps on text or chooses
* the "Select Word" context menu item. Initializes SelectionHandler,
@@ -203,6 +211,9 @@ var SelectionHandler = {
return;
}
+ // Add a listener to end the selection if it's removed programatically
+ selection.QueryInterface(Ci.nsISelectionPrivate).addSelectionListener(this);
+
// Initialize the cache
this._cache = { start: {}, end: {}};
this._updateCacheForSelection();
@@ -463,6 +474,8 @@ var SelectionHandler = {
if (this._activeType == this.TYPE_SELECTION) {
let selection = this._getSelection();
if (selection) {
+ // Remove our listener before we removeAllRanges()
+ selection.QueryInterface(Ci.nsISelectionPrivate).removeSelectionListener(this);
selection.removeAllRanges();
}
}
From 94c836784cb3f096fa7e99504eebbb639f2601af Mon Sep 17 00:00:00 2001
From: Anup Allamsetty
Date: Fri, 16 Aug 2013 13:51:42 -0700
Subject: [PATCH 24/46] Bug 896911 - Rename "Resend" item to "Edit and Resend";
r=harth
---
browser/devtools/netmonitor/netmonitor.xul | 6 +++---
.../en-US/chrome/browser/devtools/netmonitor.dtd | 12 ++++++------
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/browser/devtools/netmonitor/netmonitor.xul b/browser/devtools/netmonitor/netmonitor.xul
index 46bd1391bae9..e4a1c4138264 100644
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -25,8 +25,8 @@
accesskey="&netmonitorUI.context.copyUrl.accesskey;"
oncommand="NetMonitorView.RequestsMenu.copyUrl();"/>
@@ -314,7 +314,7 @@
crop="end"
flex="1"/>
diff --git a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
index 606a20dc2599..a265947962a9 100644
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd
@@ -177,14 +177,14 @@
- for the Copy URL menu item displayed in the context menu for a request -->
-
-
+
-
-
+
+
From d0ff61182b93d10158aa077c3ee7fab9df7a3e99 Mon Sep 17 00:00:00 2001
From: Jared Wein
Date: Fri, 16 Aug 2013 14:03:43 -0400
Subject: [PATCH 25/46] Bug 897160 - Set a minimum width for the Firefox
window. r=mconley
---
browser/base/content/browser.css | 8 ++++++++
.../components/tabview/test/browser_tabview_bug625269.js | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
index 1557205137bd..d76cfb125464 100644
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -5,6 +5,14 @@
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
@namespace html url("http://www.w3.org/1999/xhtml");
+#main-window:not([chromehidden~="toolbar"]) {
+%ifdef XP_MACOSX
+ min-width: 425px;
+%else
+ min-width: 390px;
+%endif
+}
+
searchbar {
-moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
}
diff --git a/browser/components/tabview/test/browser_tabview_bug625269.js b/browser/components/tabview/test/browser_tabview_bug625269.js
index 062fdb783983..ba4d40d2f862 100644
--- a/browser/components/tabview/test/browser_tabview_bug625269.js
+++ b/browser/components/tabview/test/browser_tabview_bug625269.js
@@ -3,7 +3,7 @@
function test() {
waitForExplicitFinish();
- newWindowWithTabView(onTabViewShown);
+ newWindowWithTabView(onTabViewShown, null, 850);
}
function onTabViewShown(win) {
From 5c4a4d62bf69ecf8f6870ae9630372e0a351c945 Mon Sep 17 00:00:00 2001
From: Wes Kocher
Date: Fri, 16 Aug 2013 13:57:21 -0700
Subject: [PATCH 26/46] Bug 906173 - Uplift addon-sdk to Firefox r=me
---
addon-sdk/source/app-extension/bootstrap.js | 2 +
.../data/test-sidebar-addon-global.html | 10 +
.../source/doc/dev-guide-source/credits.md | 2 +
.../tutorials/event-targets.md | 2 +-
addon-sdk/source/doc/module-source/sdk/ui.md | 214 +++
addon-sdk/source/lib/sdk/loader/cuddlefish.js | 1 -
addon-sdk/source/lib/sdk/simple-prefs.js | 4 +-
addon-sdk/source/lib/sdk/ui.js | 14 +
addon-sdk/source/lib/sdk/ui/button.js | 159 ++
addon-sdk/source/lib/sdk/ui/button/view.js | 201 +++
addon-sdk/source/lib/sdk/ui/sidebar.js | 330 ++++
.../source/lib/sdk/ui/sidebar/actions.js | 10 +
.../source/lib/sdk/ui/sidebar/contract.js | 38 +
.../source/lib/sdk/ui/sidebar/namespace.js | 11 +
addon-sdk/source/lib/sdk/ui/sidebar/utils.js | 8 +
addon-sdk/source/lib/sdk/ui/sidebar/view.js | 193 +++
addon-sdk/source/lib/sdk/ui/state.js | 240 +++
addon-sdk/source/lib/sdk/windows/firefox.js | 10 +-
.../source/python-lib/cuddlefish/__init__.py | 10 +-
.../python-lib/cuddlefish/options_defaults.py | 4 +-
.../python-lib/cuddlefish/options_xul.py | 4 +-
.../source/python-lib/cuddlefish/packaging.py | 15 +
.../packages/curly-id/lib/main.js | 4 +
.../packages/curly-id/package.json | 14 +
.../packages/preferences-branch/lib/main.js | 4 +
.../packages/preferences-branch/package.json | 14 +
.../cuddlefish/tests/test_packaging.py | 3 +-
.../python-lib/cuddlefish/tests/test_xpi.py | 52 +-
addon-sdk/source/python-lib/cuddlefish/xpi.py | 5 +-
.../source/test/addons/curly-id/lib/main.js | 45 +
.../source/test/addons/curly-id/package.json | 14 +
.../addons/predefined-id-with-at/lib/main.js | 35 +
.../addons/predefined-id-with-at/package.json | 11 +
.../addons/preferences-branch/lib/main.js | 36 +
.../addons/preferences-branch/package.json | 14 +
.../addons/private-browsing-supported/main.js | 1 +
.../sidebar/utils.js | 86 +
.../test-sidebar.js | 217 +++
.../test/addons/simple-prefs/lib/main.js | 5 +
.../test/addons/standard-id/lib/main.js | 45 +
.../test/addons/standard-id/package.json | 14 +
addon-sdk/source/test/sidebar/utils.js | 92 +
.../source/test/tabs/test-fennec-tabs.js | 232 ++-
.../source/test/tabs/test-firefox-tabs.js | 419 ++---
addon-sdk/source/test/test-disposable.js | 1 -
addon-sdk/source/test/test-panel.js | 19 +-
addon-sdk/source/test/test-places-history.js | 5 +-
addon-sdk/source/test/test-tabs.js | 25 +-
addon-sdk/source/test/test-test-loader.js | 2 +-
addon-sdk/source/test/test-ui-button.js | 998 +++++++++++
.../test/test-ui-sidebar-private-browsing.js | 208 +++
addon-sdk/source/test/test-ui-sidebar.js | 1490 +++++++++++++++++
addon-sdk/source/test/test-window-observer.js | 47 +-
53 files changed, 5199 insertions(+), 440 deletions(-)
create mode 100644 addon-sdk/source/data/test-sidebar-addon-global.html
create mode 100644 addon-sdk/source/doc/module-source/sdk/ui.md
create mode 100644 addon-sdk/source/lib/sdk/ui.js
create mode 100644 addon-sdk/source/lib/sdk/ui/button.js
create mode 100644 addon-sdk/source/lib/sdk/ui/button/view.js
create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar.js
create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/actions.js
create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/contract.js
create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/namespace.js
create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/utils.js
create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/view.js
create mode 100644 addon-sdk/source/lib/sdk/ui/state.js
create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/curly-id/lib/main.js
create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/curly-id/package.json
create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/preferences-branch/lib/main.js
create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/preferences-files/packages/preferences-branch/package.json
create mode 100644 addon-sdk/source/test/addons/curly-id/lib/main.js
create mode 100644 addon-sdk/source/test/addons/curly-id/package.json
create mode 100644 addon-sdk/source/test/addons/predefined-id-with-at/lib/main.js
create mode 100644 addon-sdk/source/test/addons/predefined-id-with-at/package.json
create mode 100644 addon-sdk/source/test/addons/preferences-branch/lib/main.js
create mode 100644 addon-sdk/source/test/addons/preferences-branch/package.json
create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/sidebar/utils.js
create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/test-sidebar.js
create mode 100644 addon-sdk/source/test/addons/standard-id/lib/main.js
create mode 100644 addon-sdk/source/test/addons/standard-id/package.json
create mode 100644 addon-sdk/source/test/sidebar/utils.js
create mode 100644 addon-sdk/source/test/test-ui-button.js
create mode 100644 addon-sdk/source/test/test-ui-sidebar-private-browsing.js
create mode 100644 addon-sdk/source/test/test-ui-sidebar.js
diff --git a/addon-sdk/source/app-extension/bootstrap.js b/addon-sdk/source/app-extension/bootstrap.js
index f1e191f1c5cc..6a16aa783da7 100644
--- a/addon-sdk/source/app-extension/bootstrap.js
+++ b/addon-sdk/source/app-extension/bootstrap.js
@@ -222,6 +222,8 @@ function startup(data, reasonCode) {
resultFile: options.resultFile,
// Arguments passed as --static-args
staticArgs: options.staticArgs,
+ // Add-on preferences branch name
+ preferencesBranch: options.preferencesBranch,
// Arguments related to test runner.
modules: {
diff --git a/addon-sdk/source/data/test-sidebar-addon-global.html b/addon-sdk/source/data/test-sidebar-addon-global.html
new file mode 100644
index 000000000000..cfa073959c30
--- /dev/null
+++ b/addon-sdk/source/data/test-sidebar-addon-global.html
@@ -0,0 +1,10 @@
+
+SIDEBAR TEST
diff --git a/addon-sdk/source/doc/dev-guide-source/credits.md b/addon-sdk/source/doc/dev-guide-source/credits.md
index 73de5d5fb304..8ad6acb648d1 100644
--- a/addon-sdk/source/doc/dev-guide-source/credits.md
+++ b/addon-sdk/source/doc/dev-guide-source/credits.md
@@ -70,6 +70,7 @@ We'd like to thank our many Jetpack project contributors! They include:
### J ###
+* Tomislav Jovanovic
* Eric H. Jung
### K ###
@@ -100,6 +101,7 @@ We'd like to thank our many Jetpack project contributors! They include:
* Joe R. Nassimian ([placidrage](https://github.com/placidrage))
* DÆ°Æ¡ng H. Nguyá»…n
* Nick Nguyen
+* nodeless
### O ###
diff --git a/addon-sdk/source/doc/dev-guide-source/tutorials/event-targets.md b/addon-sdk/source/doc/dev-guide-source/tutorials/event-targets.md
index 238a717426fa..6d2dc406031b 100644
--- a/addon-sdk/source/doc/dev-guide-source/tutorials/event-targets.md
+++ b/addon-sdk/source/doc/dev-guide-source/tutorials/event-targets.md
@@ -46,7 +46,7 @@ Then open "lib/main.js" and add the following code:
onItemVisited: function(aItemId, aVisitID, time) {
console.log("visited ", bookmarkService.getBookmarkURI(aItemId).spec);
},
- QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsINavBookmarkObserver])
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
};
exports.main = function() {
diff --git a/addon-sdk/source/doc/module-source/sdk/ui.md b/addon-sdk/source/doc/module-source/sdk/ui.md
new file mode 100644
index 000000000000..7ecd5f0b6afc
--- /dev/null
+++ b/addon-sdk/source/doc/module-source/sdk/ui.md
@@ -0,0 +1,214 @@
+
+
+
+
+This module exports a two constructor functions `Button` which constructs a
+new toolbar button, and `Sidebar` which constructs a sidebar (with a button).
+
+Sidebars are displayed on the left side of your browser. Its content is specified as
+local HTML, so the appearance and behaviour of the sidebar
+is limited only by what you can do using HTML, CSS and JavaScript.
+
+The screenshot below shows a sidebar whose content is built from tweets:
+
+
+
+Sidebars are useful for presenting temporary interfaces to users in a way that is
+easier for users to ignore and dismiss than a modal dialog
+and easier for users to keep around than a Panel, since sidebars are
+displayed at the side of the browser until the user decides to close it.
+
+A sidebar's content is loaded anew as soon as it is opened, and unloads
+when the user closes the sidebar.
+
+Your add-on can receive notifications when a sidebar is shown or hidden by
+listening to its `show` and `hide` events.
+
+Opening a sidebar in a window will close an already opened sidebar in that window.
+
+## Sidebar Content ##
+
+The sidebar's content is specified as HTML, which is loaded from the URL
+supplied in the `url` option to the sidebar's constructor.
+
+You can load remote HTML into the sidebar:
+
+ var sidebar = require("sdk/ui").Sidebar({
+ id: 'a-new-sidebar',
+ title: 'A New Sidebar',
+ icon: './icon.png',
+ url: './index.html'
+ });
+
+ sidebar.show();
+
+This will load HTML that's been packaged with your add-on, and this is
+most probably how you will create sidebars. To do this, save
+the `index.html` HTML file in your add-on's `data` directory.
+
+## Sidebar Positioning ##
+
+By default the sidebars appears on the left side of the currently active browser window.
+
+
+## Updating Sidebar Content ##
+
+You can update the sidebar's content simply by setting the sidebar's `url`
+property. Note this will change the sidebar's url for all windows.
+
+## Scripting Sidebar Content ##
+
+You can't directly access your sidebar's content from your main add-on code.
+To access the sidebar's content, you need to add a `";
@@ -791,9 +747,9 @@ exports.testAttachWrappers = function (test) {
' self.postMessage(e.message);' +
'}',
onMessage: function (msg) {
- test.assertEqual(msg, true, "Worker has wrapped objects ("+count+")");
+ assert.equal(msg, true, "Worker has wrapped objects ("+count+")");
if (count++ == 1)
- closeBrowserWindow(window, function() test.done());
+ close(window).then(done);
}
});
}
@@ -805,9 +761,8 @@ exports.testAttachWrappers = function (test) {
/*
// We do not offer unwrapped access to DOM since bug 601295 landed
// See 660780 to track progress of unwrap feature
-exports.testAttachUnwrapped = function (test) {
+exports.testAttachUnwrapped = function (assert, done) {
// Check that content script has access to unwrapped values through unsafeWindow
- test.waitUntilDone();
openBrowserWindow(function(window, browser) {
let document = "data:text/html;charset=utf-8,";
let count = 0;
@@ -822,8 +777,8 @@ exports.testAttachUnwrapped = function (test) {
' self.postMessage(e.message);' +
'}',
onMessage: function (msg) {
- test.assertEqual(msg, true, "Worker has access to javascript content globals ("+count+")");
- closeBrowserWindow(window, function() test.done());
+ assert.equal(msg, true, "Worker has access to javascript content globals ("+count+")");
+ close(window).then(done);
}
});
}
@@ -833,27 +788,25 @@ exports.testAttachUnwrapped = function (test) {
}
*/
-exports['test window focus changes active tab'] = function(test) {
- test.waitUntilDone();
-
+exports['test window focus changes active tab'] = function(assert, done) {
let url1 = "data:text/html;charset=utf-8," + encodeURIComponent("test window focus changes active tab
Window #1");
let win1 = openBrowserWindow(function() {
- test.pass("window 1 is open");
+ assert.pass("window 1 is open");
let win2 = openBrowserWindow(function() {
- test.pass("window 2 is open");
+ assert.pass("window 2 is open");
focus(win2).then(function() {
tabs.on("activate", function onActivate(tab) {
tabs.removeListener("activate", onActivate);
- test.pass("activate was called on windows focus change.");
- test.assertEqual(tab.url, url1, 'the activated tab url is correct');
+ assert.pass("activate was called on windows focus change.");
+ assert.equal(tab.url, url1, 'the activated tab url is correct');
close(win2).then(function() {
- test.pass('window 2 was closed');
+ assert.pass('window 2 was closed');
return close(win1);
- }).then(test.done.bind(test));
+ }).then(done);
});
win1.focus();
@@ -862,24 +815,21 @@ exports['test window focus changes active tab'] = function(test) {
}, url1);
};
-exports['test ready event on new window tab'] = function(test) {
- test.waitUntilDone();
+exports['test ready event on new window tab'] = function(assert, done) {
let uri = encodeURI("data:text/html;charset=utf-8,Waiting for ready event!");
require("sdk/tabs").on("ready", function onReady(tab) {
if (tab.url === uri) {
require("sdk/tabs").removeListener("ready", onReady);
- test.pass("ready event was emitted");
- closeBrowserWindow(window, function() {
- test.done();
- });
+ assert.pass("ready event was emitted");
+ close(window).then(done);
}
});
let window = openBrowserWindow(function(){}, uri);
};
-exports['test unique tab ids'] = function(test) {
+exports['test unique tab ids'] = function(assert, done) {
var windows = require('sdk/windows').browserWindows;
var { all, defer } = require('sdk/core/promise');
@@ -891,9 +841,9 @@ exports['test unique tab ids'] = function(test) {
});
win.on('open', function(window) {
- test.assert(window.tabs.length);
- test.assert(window.tabs.activeTab);
- test.assert(window.tabs.activeTab.id);
+ assert.ok(window.tabs.length);
+ assert.ok(window.tabs.activeTab);
+ assert.ok(window.tabs.activeTab.id);
deferred.resolve({
id: window.tabs.activeTab.id,
win: win
@@ -903,32 +853,29 @@ exports['test unique tab ids'] = function(test) {
return deferred.promise;
}
- test.waitUntilDone();
var one = openWindow(), two = openWindow();
all([one, two]).then(function(results) {
- test.assertNotEqual(results[0].id, results[1].id, "tab Ids should not be equal.");
+ assert.notEqual(results[0].id, results[1].id, "tab Ids should not be equal.");
results[0].win.close();
results[1].win.close();
- test.done();
+ done();
});
}
// related to Bug 671305
-exports.testOnLoadEventWithDOM = function(test) {
- test.waitUntilDone();
-
+exports.testOnLoadEventWithDOM = function(assert, done) {
openBrowserWindow(function(window, browser) {
let count = 0;
tabs.on('load', function onLoad(tab) {
- test.assertEqual(tab.title, 'tab', 'tab passed in as arg, load called');
+ assert.equal(tab.title, 'tab', 'tab passed in as arg, load called');
if (!count++) {
tab.reload();
}
else {
// end of test
tabs.removeListener('load', onLoad);
- test.pass('onLoad event called on reload');
- closeBrowserWindow(window, function() test.done());
+ assert.pass('onLoad event called on reload');
+ close(window).then(done);
}
});
@@ -941,9 +888,7 @@ exports.testOnLoadEventWithDOM = function(test) {
};
// related to Bug 671305
-exports.testOnLoadEventWithImage = function(test) {
- test.waitUntilDone();
-
+exports.testOnLoadEventWithImage = function(assert, done) {
openBrowserWindow(function(window, browser) {
let count = 0;
tabs.on('load', function onLoad(tab) {
@@ -953,8 +898,8 @@ exports.testOnLoadEventWithImage = function(test) {
else {
// end of test
tabs.removeListener('load', onLoad);
- test.pass('onLoad event called on reload with image');
- closeBrowserWindow(window, function() test.done());
+ assert.pass('onLoad event called on reload with image');
+ close(window).then(done);
}
});
@@ -966,23 +911,22 @@ exports.testOnLoadEventWithImage = function(test) {
});
};
-exports.testFaviconGetterDeprecation = function (test) {
+exports.testFaviconGetterDeprecation = function (assert, done) {
const { LoaderWithHookedConsole } = require("sdk/test/loader");
let { loader, messages } = LoaderWithHookedConsole(module);
let tabs = loader.require('sdk/tabs');
- test.waitUntilDone();
tabs.open({
url: 'data:text/html;charset=utf-8,',
onOpen: function (tab) {
let favicon = tab.favicon;
- test.assert(messages.length === 1, 'only one error is dispatched');
- test.assert(messages[0].type, 'error', 'the console message is an error');
+ assert.ok(messages.length === 1, 'only one error is dispatched');
+ assert.ok(messages[0].type, 'error', 'the console message is an error');
let msg = messages[0].msg;
- test.assert(msg.indexOf('tab.favicon is deprecated') !== -1,
+ assert.ok(msg.indexOf('tab.favicon is deprecated') !== -1,
'message contains the given message');
- tab.close(test.done.bind(test));
+ tab.close(done);
loader.unload();
}
});
@@ -1027,11 +971,4 @@ function openBrowserWindow(callback, url) {
return window;
}
-// Helper for calling code at window close
-function closeBrowserWindow(window, callback) {
- window.addEventListener("unload", function unload() {
- window.removeEventListener("unload", unload, false);
- callback();
- }, false);
- window.close();
-}
+require('sdk/test').run(exports);
diff --git a/addon-sdk/source/test/test-disposable.js b/addon-sdk/source/test/test-disposable.js
index a4975746906d..95b438d4fbb8 100644
--- a/addon-sdk/source/test/test-disposable.js
+++ b/addon-sdk/source/test/test-disposable.js
@@ -1,7 +1,6 @@
/* 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/. */
-
"use strict";
const { Loader } = require("sdk/test/loader");
diff --git a/addon-sdk/source/test/test-panel.js b/addon-sdk/source/test/test-panel.js
index fc8ea99a397c..687e5950ad0c 100644
--- a/addon-sdk/source/test/test-panel.js
+++ b/addon-sdk/source/test/test-panel.js
@@ -132,26 +132,27 @@ exports["test Document Reload"] = function(assert, done) {
let url2 = "data:text/html;charset=utf-8,page2";
let content =
"";
let messageCount = 0;
let panel = Panel({
// using URL here is intentional, see bug 859009
contentURL: URL("data:text/html;charset=utf-8," + encodeURIComponent(content)),
- contentScript: "self.postMessage(window.location.href)",
+ contentScript: "self.postMessage(window.location.href);" +
+ // initiate change to url2
+ "self.port.once('move', function() document.defaultView.postMessage('move', '*'));",
onMessage: function (message) {
messageCount++;
- assert.notEqual(message, 'about:blank', 'about:blank is not a message ' + messageCount);
+ assert.notEqual(message, "about:blank", "about:blank is not a message " + messageCount);
if (messageCount == 1) {
- assert.ok(/data:text\/html/.test(message), "First document had a content script " + message);
+ assert.ok(/data:text\/html/.test(message), "First document had a content script; " + message);
+ panel.port.emit('move');
}
else if (messageCount == 2) {
- assert.equal(message, url2, "Second document too");
+ assert.equal(message, url2, "Second document too; " + message);
panel.destroy();
done();
}
diff --git a/addon-sdk/source/test/test-places-history.js b/addon-sdk/source/test/test-places-history.js
index 36b57737935c..261e8001fcaf 100644
--- a/addon-sdk/source/test/test-places-history.js
+++ b/addon-sdk/source/test/test-places-history.js
@@ -87,6 +87,9 @@ exports.testSearchURL = function (assert, done) {
});
};
+// Disabling due to intermittent Bug 892619
+// TODO solve this
+/*
exports.testSearchTimeRange = function (assert, done) {
let firstTime, secondTime;
addVisits([
@@ -120,7 +123,7 @@ exports.testSearchTimeRange = function (assert, done) {
done();
});
};
-
+*/
exports.testSearchQuery = function (assert, done) {
addVisits([
'http://mozilla.com', 'http://webaud.io', 'http://mozilla.com/webfwd'
diff --git a/addon-sdk/source/test/test-tabs.js b/addon-sdk/source/test/test-tabs.js
index 775968b19ee5..84fbdb228a6e 100644
--- a/addon-sdk/source/test/test-tabs.js
+++ b/addon-sdk/source/test/test-tabs.js
@@ -3,21 +3,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
-const app = require("sdk/system/xul-app");
+module.metadata = {
+ 'engines': {
+ 'Firefox': '*',
+ 'Fennec': '*'
+ }
+};
-if (app.is("Firefox")) {
- module.exports = require("./tabs/test-firefox-tabs");
-}
-else if (app.is("Fennec")) {
- module.exports = require("./tabs/test-fennec-tabs");
+const app = require('sdk/system/xul-app');
+
+if (app.is('Fennec')) {
+ module.exports = require('./tabs/test-fennec-tabs');
}
else {
- require("test").run({
- "test Unsupported Application": function Unsupported (assert) {
- assert.pass(
- "The tabs module currently supports only Firefox and Fennec." +
- "In the future we would like it to support other applications, however."
- );
- }
- });
+ module.exports = require('./tabs/test-firefox-tabs');
}
diff --git a/addon-sdk/source/test/test-test-loader.js b/addon-sdk/source/test/test-test-loader.js
index 4ae2ffc5809d..2fe5d9ed2f10 100644
--- a/addon-sdk/source/test/test-test-loader.js
+++ b/addon-sdk/source/test/test-test-loader.js
@@ -57,4 +57,4 @@ exports["test LoaderWithHookedConsole"] = function (assert) {
assert.equal(count, 6, "Called for all messages");
};
-require("test").run(exports);
+require("sdk/test").run(exports);
diff --git a/addon-sdk/source/test/test-ui-button.js b/addon-sdk/source/test/test-ui-button.js
new file mode 100644
index 000000000000..5a0f5d5c242c
--- /dev/null
+++ b/addon-sdk/source/test/test-ui-button.js
@@ -0,0 +1,998 @@
+/* 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/. */
+'use strict';
+
+module.metadata = {
+ 'engines': {
+ 'Firefox': '> 24'
+ }
+};
+
+const { Cu } = require('chrome');
+const { Loader } = require('sdk/test/loader');
+const { data } = require('sdk/self');
+const { open, focus, close } = require('sdk/window/helpers');
+const { setTimeout } = require('sdk/timers');
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+
+function getWidget(buttonId, window = getMostRecentBrowserWindow()) {
+ const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+ const { AREA_NAVBAR } = CustomizableUI;
+
+ let widgets = CustomizableUI.getWidgetsInArea(AREA_NAVBAR).
+ filter(({id}) => id.startsWith('button--') && id.endsWith(buttonId));
+
+ if (widgets.length === 0)
+ throw new Error('Widget with id `' + id +'` not found.');
+
+ if (widgets.length > 1)
+ throw new Error('Unexpected number of widgets: ' + widgets.length)
+
+ return widgets[0].forWindow(window);
+};
+
+exports['test basic constructor validation'] = function(assert) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+
+ assert.throws(
+ () => Button({}),
+ /^The option/,
+ 'throws on no option given');
+
+ // Test no label
+ assert.throws(
+ () => Button({ id: 'my-button', icon: './icon.png'}),
+ /^The option "label"/,
+ 'throws on no label given');
+
+ // Test no id
+ assert.throws(
+ () => Button({ label: 'my button', icon: './icon.png' }),
+ /^The option "id"/,
+ 'throws on no id given');
+
+ // Test no icon
+ assert.throws(
+ () => Button({ id: 'my-button', label: 'my button' }),
+ /^The option "icon"/,
+ 'throws on no icon given');
+
+
+ // Test empty label
+ assert.throws(
+ () => Button({ id: 'my-button', label: '', icon: './icon.png' }),
+ /^The option "label"/,
+ 'throws on no valid label given');
+
+ // Test invalid id
+ assert.throws(
+ () => Button({ id: 'my button', label: 'my button', icon: './icon.png' }),
+ /^The option "id"/,
+ 'throws on no valid id given');
+
+ // Test empty id
+ assert.throws(
+ () => Button({ id: '', label: 'my button', icon: './icon.png' }),
+ /^The option "id"/,
+ 'throws on no valid id given');
+
+ // Test remote icon
+ assert.throws(
+ () => Button({ id: 'my-button', label: 'my button', icon: 'http://www.mozilla.org/favicon.ico'}),
+ /^The option "icon"/,
+ 'throws on no valid icon given');
+
+ // Test wrong icon: no absolute URI to local resource, neither relative './'
+ assert.throws(
+ () => Button({ id: 'my-button', label: 'my button', icon: 'icon.png'}),
+ /^The option "icon"/,
+ 'throws on no valid icon given');
+
+ // Test wrong icon: no absolute URI to local resource, neither relative './'
+ assert.throws(
+ () => Button({ id: 'my-button', label: 'my button', icon: 'foo and bar'}),
+ /^The option "icon"/,
+ 'throws on no valid icon given');
+
+ // Test wrong icon: '../' is not allowed
+ assert.throws(
+ () => Button({ id: 'my-button', label: 'my button', icon: '../icon.png'}),
+ /^The option "icon"/,
+ 'throws on no valid icon given');
+
+ // Test wrong size: number
+ assert.throws(
+ () => Button({
+ id:'my-button',
+ label: 'my button',
+ icon: './icon.png',
+ size: 32
+ }),
+ /^The option "size"/,
+ 'throws on no valid size given');
+
+ // Test wrong size: string
+ assert.throws(
+ () => Button({
+ id:'my-button',
+ label: 'my button',
+ icon: './icon.png',
+ size: 'huge'
+ }),
+ /^The option "size"/,
+ 'throws on no valid size given');
+
+ // Test wrong type
+ assert.throws(
+ () => Button({
+ id:'my-button',
+ label: 'my button',
+ icon: './icon.png',
+ type: 'custom'
+ }),
+ /^The option "type"/,
+ 'throws on no valid type given');
+
+ loader.unload();
+};
+
+exports['test button added'] = function(assert) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+
+ let button = Button({
+ id: 'my-button-1',
+ label: 'my button',
+ icon: './icon.png'
+ });
+
+ // check defaults
+ assert.equal(button.size, 'small',
+ 'size is set to default "small" value');
+
+ assert.equal(button.disabled, false,
+ 'disabled is set to default `false` value');
+
+ assert.equal(button.checked, false,
+ 'checked is set to default `false` value');
+
+ assert.equal(button.type, 'button',
+ 'type is set to default "button" value');
+
+ let { node } = getWidget(button.id);
+
+ assert.ok(!!node, 'The button is in the navbar');
+
+ assert.equal(button.label, node.getAttribute('label'),
+ 'label is set');
+
+ assert.equal(button.label, node.getAttribute('tooltiptext'),
+ 'tooltip is set');
+
+ assert.equal(data.url(button.icon.substr(2)), node.getAttribute('image'),
+ 'icon is set');
+
+ assert.equal(button.type, node.getAttribute('type'),
+ 'type is set to default');
+
+ assert.equal(16, node.getAttribute('width'),
+ 'width is set to small');
+
+ loader.unload();
+}
+
+exports['test button added with resource URI'] = function(assert) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+
+ let button = Button({
+ id: 'my-button-1',
+ label: 'my button',
+ icon: data.url('icon.png')
+ });
+
+ assert.equal(button.icon, data.url('icon.png'),
+ 'icon is set');
+
+ let { node } = getWidget(button.id);
+
+ assert.equal(button.icon, node.getAttribute('image'),
+ 'icon on node is set');
+
+ loader.unload();
+}
+
+exports['test button duplicate id'] = function(assert) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+
+ let button = Button({
+ id: 'my-button-2',
+ label: 'my button',
+ icon: './icon.png'
+ });
+
+ assert.throws(() => {
+ let doppelganger = Button({
+ id: 'my-button-2',
+ label: 'my button',
+ icon: './icon.png'
+ });
+ },
+ /^The ID/,
+ 'No duplicates allowed');
+
+ loader.unload();
+}
+
+exports['test button multiple destroy'] = function(assert) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+
+ let button = Button({
+ id: 'my-button-2',
+ label: 'my button',
+ icon: './icon.png'
+ });
+
+ button.destroy();
+ button.destroy();
+ button.destroy();
+
+ assert.pass('multiple destroy doesn\'t matter');
+
+ loader.unload();
+}
+
+exports['test button removed on dispose'] = function(assert, done) {
+ const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+
+ let widgetId;
+
+ CustomizableUI.addListener({
+ onWidgetDestroyed: function(id) {
+ if (id === widgetId) {
+ CustomizableUI.removeListener(this);
+
+ assert.pass('button properly removed');
+ loader.unload();
+ done();
+ }
+ }
+ });
+
+ let button = Button({
+ id: 'my-button-3',
+ label: 'my button',
+ icon: './icon.png'
+ });
+
+ // Tried to use `getWidgetIdsInArea` but seems undefined, not sure if it
+ // was removed or it's not in the UX build yet
+ widgetId = getWidget(button.id).id;
+
+ button.destroy();
+};
+
+exports['test button global state updated'] = function(assert) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+
+ let button = Button({
+ id: 'my-button-4',
+ label: 'my button',
+ icon: './icon.png'
+ });
+
+ // Tried to use `getWidgetIdsInArea` but seems undefined, not sure if it
+ // was removed or it's not in the UX build yet
+
+ let { node, id: widgetId } = getWidget(button.id);
+
+ // check read-only properties
+
+ assert.throws(() => button.id = 'another-id',
+ /^setting a property that has only a getter/,
+ 'id cannot be set at runtime');
+
+ assert.equal(button.id, 'my-button-4',
+ 'id is unchanged');
+ assert.equal(node.id, widgetId,
+ 'node id is unchanged');
+
+ assert.throws(() => button.type = 'checkbox',
+ /^setting a property that has only a getter/,
+ 'type cannot be set at runtime');
+
+ assert.equal(button.type, 'button',
+ 'type is unchanged');
+ assert.equal(node.getAttribute('type'), button.type,
+ 'node type is unchanged');
+
+ assert.throws(() => button.size = 'medium',
+ /^setting a property that has only a getter/,
+ 'size cannot be set at runtime');
+
+ assert.equal(button.size, 'small',
+ 'size is unchanged');
+ assert.equal(node.getAttribute('width'), 16,
+ 'node width is unchanged');
+
+ // check writable properties
+
+ button.label = 'New label';
+ assert.equal(button.label, 'New label',
+ 'label is updated');
+ assert.equal(node.getAttribute('label'), 'New label',
+ 'node label is updated');
+ assert.equal(node.getAttribute('tooltiptext'), 'New label',
+ 'node tooltip is updated');
+
+ button.icon = './new-icon.png';
+ assert.equal(button.icon, './new-icon.png',
+ 'icon is updated');
+ assert.equal(node.getAttribute('image'), data.url('new-icon.png'),
+ 'node image is updated');
+
+ button.disabled = true;
+ assert.equal(button.disabled, true,
+ 'disabled is updated');
+ assert.equal(node.getAttribute('disabled'), 'true',
+ 'node disabled is updated');
+
+ // TODO: test validation on update
+
+ loader.unload();
+}
+
+exports['test button global state updated on multiple windows'] = function(assert, done) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+
+ let button = Button({
+ id: 'my-button-5',
+ label: 'my button',
+ icon: './icon.png'
+ });
+
+ let nodes = [getWidget(button.id).node];
+
+ open(null, { features: { toolbar: true }}).then(window => {
+ nodes.push(getWidget(button.id, window).node);
+
+ button.label = 'New label';
+ button.icon = './new-icon.png';
+ button.disabled = true;
+
+ for (let node of nodes) {
+ assert.equal(node.getAttribute('label'), 'New label',
+ 'node label is updated');
+ assert.equal(node.getAttribute('tooltiptext'), 'New label',
+ 'node tooltip is updated');
+
+ assert.equal(button.icon, './new-icon.png',
+ 'icon is updated');
+ assert.equal(node.getAttribute('image'), data.url('new-icon.png'),
+ 'node image is updated');
+
+ assert.equal(button.disabled, true,
+ 'disabled is updated');
+ assert.equal(node.getAttribute('disabled'), 'true',
+ 'node disabled is updated');
+ };
+
+ return window;
+ }).
+ then(close).
+ then(loader.unload).
+ then(done, assert.fail);
+};
+
+exports['test button window state'] = function(assert, done) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+ let { browserWindows } = loader.require('sdk/windows');
+
+ let button = Button({
+ id: 'my-button-6',
+ label: 'my button',
+ icon: './icon.png'
+ });
+
+ let mainWindow = browserWindows.activeWindow;
+ let nodes = [getWidget(button.id).node];
+
+ open(null, { features: { toolbar: true }}).then(focus).then(window => {
+ nodes.push(getWidget(button.id, window).node);
+
+ let { activeWindow } = browserWindows;
+
+ button.state(activeWindow, {
+ label: 'New label',
+ icon: './new-icon.png',
+ disabled: true
+ });
+
+ // check the states
+
+ assert.equal(button.label, 'my button',
+ 'global label unchanged');
+ assert.equal(button.icon, './icon.png',
+ 'global icon unchanged');
+ assert.equal(button.disabled, false,
+ 'global disabled unchanged');
+
+ let state = button.state(mainWindow);
+
+ assert.equal(state.label, 'my button',
+ 'previous window label unchanged');
+ assert.equal(state.icon, './icon.png',
+ 'previous window icon unchanged');
+ assert.equal(state.disabled, false,
+ 'previous window disabled unchanged');
+
+ let state = button.state(activeWindow);
+
+ assert.equal(state.label, 'New label',
+ 'active window label updated');
+ assert.equal(state.icon, './new-icon.png',
+ 'active window icon updated');
+ assert.equal(state.disabled, true,
+ 'active disabled updated');
+
+ // change the global state, only the windows without a state are affected
+
+ button.label = 'A good label';
+
+ assert.equal(button.label, 'A good label',
+ 'global label updated');
+ assert.equal(button.state(mainWindow).label, 'A good label',
+ 'previous window label updated');
+ assert.equal(button.state(activeWindow).label, 'New label',
+ 'active window label unchanged');
+
+ // delete the window state will inherits the global state again
+
+ button.state(activeWindow, null);
+
+ assert.equal(button.state(activeWindow).label, 'A good label',
+ 'active window label inherited');
+
+ // check the nodes properties
+ let node = nodes[0];
+ let state = button.state(mainWindow);
+
+ assert.equal(node.getAttribute('label'), state.label,
+ 'node label is correct');
+ assert.equal(node.getAttribute('tooltiptext'), state.label,
+ 'node tooltip is correct');
+
+ assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
+ 'node image is correct');
+ assert.equal(node.hasAttribute('disabled'), state.disabled,
+ 'disabled is correct');
+
+ let node = nodes[1];
+ let state = button.state(activeWindow);
+
+ assert.equal(node.getAttribute('label'), state.label,
+ 'node label is correct');
+ assert.equal(node.getAttribute('tooltiptext'), state.label,
+ 'node tooltip is correct');
+
+ assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
+ 'node image is correct');
+ assert.equal(node.hasAttribute('disabled'), state.disabled,
+ 'disabled is correct');
+
+ return window;
+ }).
+ then(close).
+ then(loader.unload).
+ then(done, assert.fail);
+};
+
+
+exports['test button tab state'] = function(assert, done) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+ let { browserWindows } = loader.require('sdk/windows');
+ let tabs = loader.require('sdk/tabs');
+
+ let button = Button({
+ id: 'my-button-7',
+ label: 'my button',
+ icon: './icon.png'
+ });
+
+ let mainTab = tabs.activeTab;
+ let node = getWidget(button.id).node;
+
+ tabs.open({
+ url: 'about:blank',
+ onActivate: function onActivate(tab) {
+ tab.removeListener('activate', onActivate);
+
+ let { activeWindow } = browserWindows;
+ // set window state
+ button.state(activeWindow, {
+ label: 'Window label',
+ icon: './window-icon.png'
+ });
+
+ // set previous active tab state
+ button.state(mainTab, {
+ label: 'Tab label',
+ icon: './tab-icon.png',
+ });
+
+ // set current active tab state
+ button.state(tab, {
+ icon: './another-tab-icon.png',
+ disabled: true
+ });
+
+ // check the states
+
+ Cu.schedulePreciseGC(() => {
+ assert.equal(button.label, 'my button',
+ 'global label unchanged');
+ assert.equal(button.icon, './icon.png',
+ 'global icon unchanged');
+ assert.equal(button.disabled, false,
+ 'global disabled unchanged');
+
+ let state = button.state(mainTab);
+
+ assert.equal(state.label, 'Tab label',
+ 'previous tab label updated');
+ assert.equal(state.icon, './tab-icon.png',
+ 'previous tab icon updated');
+ assert.equal(state.disabled, false,
+ 'previous tab disabled unchanged');
+
+ let state = button.state(tab);
+
+ assert.equal(state.label, 'Window label',
+ 'active tab inherited from window state');
+ assert.equal(state.icon, './another-tab-icon.png',
+ 'active tab icon updated');
+ assert.equal(state.disabled, true,
+ 'active disabled updated');
+
+ // change the global state
+ button.icon = './good-icon.png';
+
+ // delete the tab state
+ button.state(tab, null);
+
+ assert.equal(button.icon, './good-icon.png',
+ 'global icon updated');
+ assert.equal(button.state(mainTab).icon, './tab-icon.png',
+ 'previous tab icon unchanged');
+ assert.equal(button.state(tab).icon, './window-icon.png',
+ 'tab icon inherited from window');
+
+ // delete the window state
+ button.state(activeWindow, null);
+
+ assert.equal(button.state(tab).icon, './good-icon.png',
+ 'tab icon inherited from global');
+
+ // check the node properties
+
+ let state = button.state(tabs.activeTab);
+
+ assert.equal(node.getAttribute('label'), state.label,
+ 'node label is correct');
+ assert.equal(node.getAttribute('tooltiptext'), state.label,
+ 'node tooltip is correct');
+ assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
+ 'node image is correct');
+ assert.equal(node.hasAttribute('disabled'), state.disabled,
+ 'disabled is correct');
+
+ tabs.once('activate', () => {
+ // This is made in order to avoid to check the node before it
+ // is updated, need a better check
+ setTimeout(() => {
+ let state = button.state(mainTab);
+
+ assert.equal(node.getAttribute('label'), state.label,
+ 'node label is correct');
+ assert.equal(node.getAttribute('tooltiptext'), state.label,
+ 'node tooltip is correct');
+ assert.equal(node.getAttribute('image'), data.url(state.icon.substr(2)),
+ 'node image is correct');
+ assert.equal(node.hasAttribute('disabled'), state.disabled,
+ 'disabled is correct');
+
+ tab.close(() => {
+ loader.unload();
+ done();
+ });
+ }, 500);
+ });
+
+ mainTab.activate();
+ });
+ }
+ });
+
+};
+
+exports['test button click'] = function(assert, done) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+ let { browserWindows } = loader.require('sdk/windows');
+
+ let labels = [];
+
+ let button = Button({
+ id: 'my-button-8',
+ label: 'my button',
+ icon: './icon.png',
+ onClick: ({label}) => labels.push(label)
+ });
+
+ let mainWindow = browserWindows.activeWindow;
+ let chromeWindow = getMostRecentBrowserWindow();
+
+ open(null, { features: { toolbar: true }}).then(focus).then(window => {
+ button.state(mainWindow, { label: 'nothing' });
+ button.state(mainWindow.tabs.activeTab, { label: 'foo'})
+ button.state(browserWindows.activeWindow, { label: 'bar' });
+
+ button.click();
+
+ focus(chromeWindow).then(() => {
+ button.click();
+
+ assert.deepEqual(labels, ['bar', 'foo'],
+ 'button click works');
+
+ close(window).
+ then(loader.unload).
+ then(done, assert.fail);
+ });
+ }).then(null, assert.fail);
+}
+
+exports['test button type checkbox'] = function(assert, done) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+ let { browserWindows } = loader.require('sdk/windows');
+
+ let events = [];
+
+ let button = Button({
+ id: 'my-button-9',
+ label: 'my button',
+ icon: './icon.png',
+ type: 'checkbox',
+ onClick: ({label}) => events.push('clicked:' + label),
+ onChange: state => events.push('changed:' + state.label + ':' + state.checked)
+ });
+
+ let { node } = getWidget(button.id);
+
+ assert.equal(button.type, 'checkbox',
+ 'button type is set');
+ assert.equal(node.getAttribute('type'), 'checkbox',
+ 'node type is set');
+
+ let mainWindow = browserWindows.activeWindow;
+ let chromeWindow = getMostRecentBrowserWindow();
+
+ open(null, { features: { toolbar: true }}).then(focus).then(window => {
+ button.state(mainWindow, { label: 'nothing' });
+ button.state(mainWindow.tabs.activeTab, { label: 'foo'})
+ button.state(browserWindows.activeWindow, { label: 'bar' });
+
+ button.click();
+ button.click();
+
+ focus(chromeWindow).then(() => {
+ button.click();
+ button.click();
+
+ assert.deepEqual(events, [
+ 'clicked:bar', 'changed:bar:true', 'clicked:bar', 'changed:bar:false',
+ 'clicked:foo', 'changed:foo:true', 'clicked:foo', 'changed:foo:false'
+ ],
+ 'button change events works');
+
+ close(window).
+ then(loader.unload).
+ then(done, assert.fail);
+ })
+ }).then(null, assert.fail);
+}
+
+exports['test button icon set'] = function(assert) {
+ const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+
+ // Test remote icon set
+ assert.throws(
+ () => Button({
+ id: 'my-button-10',
+ label: 'my button',
+ icon: {
+ '16': 'http://www.mozilla.org/favicon.ico'
+ }
+ }),
+ /^The option "icon"/,
+ 'throws on no valid icon given');
+
+ let button = Button({
+ id: 'my-button-11',
+ label: 'my button',
+ icon: {
+ '5': './icon5.png',
+ '16': './icon16.png',
+ '32': './icon32.png',
+ '64': './icon64.png'
+ }
+ });
+
+ let { node, id: widgetId } = getWidget(button.id);
+ let { devicePixelRatio } = node.ownerDocument.defaultView;
+
+ let size = 16 * devicePixelRatio;
+
+ assert.equal(node.getAttribute('image'), data.url(button.icon[size].substr(2)),
+ 'the icon is set properly in navbar');
+
+ let size = 32 * devicePixelRatio;
+
+ CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_PANEL);
+
+ assert.equal(node.getAttribute('image'), data.url(button.icon[size].substr(2)),
+ 'the icon is set properly in panel');
+
+ // Using `loader.unload` without move back the button to the original area
+ // raises an error in the CustomizableUI. This is doesn't happen if the
+ // button is moved manually from navbar to panel. I believe it has to do
+ // with `addWidgetToArea` method, because even with a `timeout` the issue
+ // persist.
+ CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR);
+
+ loader.unload();
+}
+
+exports['test button icon se with only one option'] = function(assert) {
+ const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+
+ // Test remote icon set
+ assert.throws(
+ () => Button({
+ id: 'my-button-10',
+ label: 'my button',
+ icon: {
+ '16': 'http://www.mozilla.org/favicon.ico'
+ }
+ }),
+ /^The option "icon"/,
+ 'throws on no valid icon given');
+
+ let button = Button({
+ id: 'my-button-11',
+ label: 'my button',
+ icon: {
+ '5': './icon5.png'
+ }
+ });
+
+ let { node, id: widgetId } = getWidget(button.id);
+
+ assert.equal(node.getAttribute('image'), data.url(button.icon['5'].substr(2)),
+ 'the icon is set properly in navbar');
+
+ CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_PANEL);
+
+ assert.equal(node.getAttribute('image'), data.url(button.icon['5'].substr(2)),
+ 'the icon is set properly in panel');
+
+ // Using `loader.unload` without move back the button to the original area
+ // raises an error in the CustomizableUI. This is doesn't happen if the
+ // button is moved manually from navbar to panel. I believe it has to do
+ // with `addWidgetToArea` method, because even with a `timeout` the issue
+ // persist.
+ CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR);
+
+ loader.unload();
+}
+
+exports['test button state validation'] = function(assert) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+ let { browserWindows } = loader.require('sdk/windows');
+
+ let button = Button({
+ id: 'my-button-12',
+ label: 'my button',
+ icon: './icon.png'
+ })
+
+ button.state(button, {
+ size: 'large'
+ });
+
+ assert.equal(button.size, 'small',
+ 'button.size is unchanged');
+
+ let state = button.state(button);
+
+ assert.equal(button.size, 'small',
+ 'button state is unchanged');
+
+ assert.throws(
+ () => button.state(button, { icon: 'http://www.mozilla.org/favicon.ico' }),
+ /^The option "icon"/,
+ 'throws on remote icon given');
+
+ loader.unload();
+};
+
+exports['test button are not in private windows'] = function(assert, done) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+ let{ isPrivate } = loader.require('sdk/private-browsing');
+ let { browserWindows } = loader.require('sdk/windows');
+
+ let button = Button({
+ id: 'my-button-13',
+ label: 'my button',
+ icon: './icon.png'
+ });
+
+ open(null, { features: { toolbar: true, private: true }}).then(window => {
+ assert.ok(isPrivate(window),
+ 'the new window is private');
+
+ let { node } = getWidget(button.id, window);
+
+ assert.ok(!node || node.style.display === 'none',
+ 'the button is not added / is not visible on private window');
+
+ return window;
+ }).
+ then(close).
+ then(loader.unload).
+ then(done, assert.fail)
+}
+
+exports['test button state are snapshot'] = function(assert) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+ let { browserWindows } = loader.require('sdk/windows');
+ let tabs = loader.require('sdk/tabs');
+
+ let button = Button({
+ id: 'my-button-14',
+ label: 'my button',
+ icon: './icon.png'
+ });
+
+ let state = button.state(button);
+ let windowState = button.state(browserWindows.activeWindow);
+ let tabState = button.state(tabs.activeTab);
+
+ assert.deepEqual(windowState, state,
+ 'window state has the same properties of button state');
+
+ assert.deepEqual(tabState, state,
+ 'tab state has the same properties of button state');
+
+ assert.notEqual(windowState, state,
+ 'window state is not the same object of button state');
+
+ assert.notEqual(tabState, state,
+ 'tab state is not the same object of button state');
+
+ assert.deepEqual(button.state(button), state,
+ 'button state has the same content of previous button state');
+
+ assert.deepEqual(button.state(browserWindows.activeWindow), windowState,
+ 'window state has the same content of previous window state');
+
+ assert.deepEqual(button.state(tabs.activeTab), tabState,
+ 'tab state has the same content of previous tab state');
+
+ assert.notEqual(button.state(button), state,
+ 'button state is not the same object of previous button state');
+
+ assert.notEqual(button.state(browserWindows.activeWindow), windowState,
+ 'window state is not the same object of previous window state');
+
+ assert.notEqual(button.state(tabs.activeTab), tabState,
+ 'tab state is not the same object of previous tab state');
+
+ loader.unload();
+}
+
+exports['test button after destroy'] = function(assert) {
+ let loader = Loader(module);
+ let { Button } = loader.require('sdk/ui');
+ let { browserWindows } = loader.require('sdk/windows');
+ let { activeTab } = loader.require('sdk/tabs');
+
+ let button = Button({
+ id: 'my-button-15',
+ label: 'my button',
+ icon: './icon.png',
+ onClick: () => assert.fail('onClick should not be called')
+ });
+
+ button.destroy();
+
+ assert.throws(
+ () => button.click(),
+ /^The state cannot be set or get/,
+ 'button.click() not executed');
+
+ assert.throws(
+ () => button.label,
+ /^The state cannot be set or get/,
+ 'button.label cannot be get after destroy');
+
+ assert.throws(
+ () => button.label = 'my label',
+ /^The state cannot be set or get/,
+ 'button.label cannot be set after destroy');
+
+ assert.throws(
+ () => {
+ button.state(browserWindows.activeWindow, {
+ label: 'window label'
+ });
+ },
+ /^The state cannot be set or get/,
+ 'window state label cannot be set after destroy');
+
+ assert.throws(
+ () => button.state(browserWindows.activeWindow).label,
+ /^The state cannot be set or get/,
+ 'window state label cannot be get after destroy');
+
+ assert.throws(
+ () => {
+ button.state(activeTab, {
+ label: 'tab label'
+ });
+ },
+ /^The state cannot be set or get/,
+ 'tab state label cannot be set after destroy');
+
+ assert.throws(
+ () => button.state(activeTab).label,
+ /^The state cannot be set or get/,
+ 'window state label cannot se get after destroy');
+
+ loader.unload();
+};
+
+// If the module doesn't support the app we're being run in, require() will
+// throw. In that case, remove all tests above from exports, and add one dummy
+// test that passes.
+try {
+ require('sdk/ui/button');
+}
+catch (err) {
+ if (!/^Unsupported Application/.test(err.message))
+ throw err;
+
+ module.exports = {
+ 'test Unsupported Application': assert => assert.pass(err.message)
+ }
+}
+
+require('sdk/test').run(exports);
diff --git a/addon-sdk/source/test/test-ui-sidebar-private-browsing.js b/addon-sdk/source/test/test-ui-sidebar-private-browsing.js
new file mode 100644
index 000000000000..3e8f066512e9
--- /dev/null
+++ b/addon-sdk/source/test/test-ui-sidebar-private-browsing.js
@@ -0,0 +1,208 @@
+/* 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/. */
+'use strict';
+
+module.metadata = {
+ 'engines': {
+ 'Firefox': '> 24'
+ }
+};
+
+const { Loader } = require('sdk/test/loader');
+const { show, hide } = require('sdk/ui/sidebar/actions');
+const { isShowing } = require('sdk/ui/sidebar/utils');
+const { getMostRecentBrowserWindow, isWindowPrivate } = require('sdk/window/utils');
+const { open, close, focus, promise: windowPromise } = require('sdk/window/helpers');
+const { setTimeout } = require('sdk/timers');
+const { isPrivate } = require('sdk/private-browsing');
+const { data } = require('sdk/self');
+const { URL } = require('sdk/url');
+
+const { BLANK_IMG, BUILTIN_SIDEBAR_MENUITEMS, isSidebarShowing,
+ getSidebarMenuitems, getExtraSidebarMenuitems, makeID, simulateCommand,
+ simulateClick, getWidget, isChecked } = require('./sidebar/utils');
+
+exports.testSideBarIsNotInNewPrivateWindows = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testSideBarIsNotInNewPrivateWindows';
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+
+ let startWindow = getMostRecentBrowserWindow();
+ let ele = startWindow.document.getElementById(makeID(testName));
+ assert.ok(ele, 'sidebar element was added');
+
+ open(null, { features: { private: true } }).then(function(window) {
+ let ele = window.document.getElementById(makeID(testName));
+ assert.ok(isPrivate(window), 'the new window is private');
+ assert.equal(ele, null, 'sidebar element was not added');
+
+ sidebar.destroy();
+ assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+ assert.ok(!startWindow.document.getElementById(makeID(testName)), 'sidebar id DNE');
+
+ close(window).then(done, assert.fail);
+ })
+}
+
+/*
+exports.testSidebarIsNotOpenInNewPrivateWindow = function(assert, done) {
+ let testName = 'testSidebarIsNotOpenInNewPrivateWindow';
+ let window = getMostRecentBrowserWindow();
+
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+
+ sidebar.on('show', function() {
+ assert.equal(isPrivate(window), false, 'the new window is not private');
+ assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+ assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+ let window2 = window.OpenBrowserWindow({private: true});
+ windowPromise(window2, 'load').then(focus).then(function() {
+ // TODO: find better alt to setTimeout...
+ setTimeout(function() {
+ assert.equal(isPrivate(window2), true, 'the new window is private');
+ assert.equal(isSidebarShowing(window), true, 'the sidebar is showing in old window still');
+ assert.equal(isSidebarShowing(window2), false, 'the sidebar is not showing in the new private window');
+ assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+ sidebar.destroy();
+ close(window2).then(done);
+ }, 500)
+ })
+ });
+
+ sidebar.show();
+}
+*/
+
+// TEST: edge case where web panel is destroyed while loading
+exports.testDestroyEdgeCaseBugWithPrivateWindow = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testDestroyEdgeCaseBug';
+ let window = getMostRecentBrowserWindow();
+
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+
+ // NOTE: purposely not listening to show event b/c the event happens
+ // between now and then.
+ sidebar.show();
+
+ assert.equal(isPrivate(window), false, 'the new window is not private');
+ assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+
+ //assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+ open(null, { features: { private: true } }).then(focus).then(function(window2) {
+ assert.equal(isPrivate(window2), true, 'the new window is private');
+ assert.equal(isSidebarShowing(window2), false, 'the sidebar is not showing');
+ assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+
+ sidebar.destroy();
+ assert.pass('destroying the sidebar');
+
+ close(window2).then(function() {
+ let loader = Loader(module);
+
+ assert.equal(isPrivate(window), false, 'the current window is not private');
+
+ let sidebar = loader.require('sdk/ui/sidebar').Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+ testName,
+ onShow: function() {
+ assert.pass('onShow works for Sidebar');
+ loader.unload();
+
+ let sidebarMI = getSidebarMenuitems();
+ for each (let mi in sidebarMI) {
+ assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+ assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
+ }
+ assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+ assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+ done();
+ }
+ })
+
+ sidebar.show();
+ assert.pass('showing the sidebar');
+
+ });
+ });
+}
+
+exports.testShowInPrivateWindow = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testShowInPrivateWindow';
+ let window = getMostRecentBrowserWindow();
+ let { document } = window;
+ let url = 'data:text/html;charset=utf-8,'+testName;
+
+ let sidebar1 = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ assert.equal(sidebar1.url, url, 'url getter works');
+ assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+ assert.ok(!isChecked(document.getElementById(makeID(sidebar1.id))),
+ 'the menuitem is not checked');
+ assert.equal(isSidebarShowing(window), false, 'the new window sidebar is not showing');
+
+ windowPromise(window.OpenBrowserWindow({ private: true }), 'load').then(function(window) {
+ let { document } = window;
+ assert.equal(isWindowPrivate(window), true, 'new window is private');
+ assert.equal(isPrivate(window), true, 'new window is private');
+
+ sidebar1.show().then(
+ function bad() {
+ assert.fail('a successful show should not happen here..');
+ },
+ function good() {
+ assert.equal(isShowing(sidebar1), false, 'the sidebar is still not showing');
+ assert.equal(document.getElementById(makeID(sidebar1.id)),
+ null,
+ 'the menuitem dne on the private window');
+ assert.equal(isSidebarShowing(window), false, 'the new window sidebar is not showing');
+
+ sidebar1.destroy();
+ close(window).then(done);
+ });
+ }, assert.fail);
+}
+
+// If the module doesn't support the app we're being run in, require() will
+// throw. In that case, remove all tests above from exports, and add one dummy
+// test that passes.
+try {
+ require('sdk/ui/sidebar');
+}
+catch (err) {
+ if (!/^Unsupported Application/.test(err.message))
+ throw err;
+
+ module.exports = {
+ 'test Unsupported Application': assert => assert.pass(err.message)
+ }
+}
+
+require('sdk/test').run(exports);
diff --git a/addon-sdk/source/test/test-ui-sidebar.js b/addon-sdk/source/test/test-ui-sidebar.js
new file mode 100644
index 000000000000..b37e8f127eef
--- /dev/null
+++ b/addon-sdk/source/test/test-ui-sidebar.js
@@ -0,0 +1,1490 @@
+/* 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/. */
+'use strict';
+
+module.metadata = {
+ 'engines': {
+ 'Firefox': '> 24'
+ }
+};
+
+const { Cu } = require('chrome');
+const { Loader } = require('sdk/test/loader');
+const { show, hide } = require('sdk/ui/sidebar/actions');
+const { isShowing } = require('sdk/ui/sidebar/utils');
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { open, close, focus, promise: windowPromise } = require('sdk/window/helpers');
+const { setTimeout } = require('sdk/timers');
+const { isPrivate } = require('sdk/private-browsing');
+const { data } = require('sdk/self');
+const { URL } = require('sdk/url');
+const { once, off, emit } = require('sdk/event/core');
+const { defer, all } = require('sdk/core/promise');
+
+const { BLANK_IMG, BUILTIN_SIDEBAR_MENUITEMS, isSidebarShowing,
+ getSidebarMenuitems, getExtraSidebarMenuitems, makeID, simulateCommand,
+ simulateClick, getWidget, isChecked } = require('./sidebar/utils');
+
+exports.testSidebarBasicLifeCycle = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testSidebarBasicLifeCycle';
+ let window = getMostRecentBrowserWindow();
+ assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+ let sidebarXUL = window.document.getElementById('sidebar');
+ assert.ok(sidebarXUL, 'sidebar xul element does exist');
+ assert.ok(!getExtraSidebarMenuitems().length, 'there are no extra sidebar menuitems');
+
+ assert.equal(isSidebarShowing(window), false, 'sidebar is not showing 1');
+ let sidebarDetails = {
+ id: testName,
+ title: 'test',
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ };
+ let sidebar = Sidebar(sidebarDetails);
+
+ // test the sidebar attributes
+ for each(let key in Object.keys(sidebarDetails)) {
+ if (key == 'icon')
+ continue;
+ assert.equal(sidebarDetails[key], sidebar[key], 'the attributes match the input');
+ }
+
+ assert.pass('The Sidebar constructor worked');
+
+ let extraMenuitems = getExtraSidebarMenuitems();
+ assert.equal(extraMenuitems.length, 1, 'there is one extra sidebar menuitems');
+
+ let ele = window.document.getElementById(makeID(testName));
+ assert.equal(ele, extraMenuitems[0], 'the only extra menuitem is the one for our sidebar.')
+ assert.ok(ele, 'sidebar element was added');
+ assert.ok(!isChecked(ele), 'the sidebar is not displayed');
+ assert.equal(ele.getAttribute('label'), sidebar.title, 'the sidebar title is the menuitem label')
+
+ assert.equal(isSidebarShowing(window), false, 'sidebar is not showing 2');
+ sidebar.on('show', function() {
+ assert.pass('the show event was fired');
+ assert.equal(isSidebarShowing(window), true, 'sidebar is not showing 3');
+ assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+ assert.ok(isChecked(ele), 'the sidebar is displayed');
+
+ sidebar.once('hide', function() {
+ assert.pass('the hide event was fired');
+ assert.ok(!isChecked(ele), 'the sidebar menuitem is not checked');
+ assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+ assert.equal(isSidebarShowing(window), false, 'the sidebar elemnt is hidden');
+
+ sidebar.once('detach', function() {
+ // calling destroy twice should not matter
+ sidebar.destroy();
+ sidebar.destroy();
+
+ let sidebarMI = getSidebarMenuitems();
+ for each (let mi in sidebarMI) {
+ assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+ assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
+ }
+
+ assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+ assert.pass('calling destroy worked without error');
+
+ done();
+ });
+ });
+
+ sidebar.hide();
+ assert.pass('hiding sidebar..');
+ });
+
+ sidebar.show();
+ assert.pass('showing sidebar..');
+}
+
+exports.testSideBarIsInNewWindows = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testSideBarIsInNewWindows';
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+
+ let startWindow = getMostRecentBrowserWindow();
+ let ele = startWindow.document.getElementById(makeID(testName));
+ assert.ok(ele, 'sidebar element was added');
+
+ open().then(function(window) {
+ let ele = window.document.getElementById(makeID(testName));
+ assert.ok(ele, 'sidebar element was added');
+
+ // calling destroy twice should not matter
+ sidebar.destroy();
+ sidebar.destroy();
+
+ assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+ assert.ok(!startWindow.document.getElementById(makeID(testName)), 'sidebar id DNE');
+
+ close(window).then(done, assert.fail);
+ })
+}
+
+exports.testSideBarIsShowingInNewWindows = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testSideBarIsShowingInNewWindows';
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: URL('data:text/html;charset=utf-8,'+testName)
+ });
+
+ let startWindow = getMostRecentBrowserWindow();
+ let ele = startWindow.document.getElementById(makeID(testName));
+ assert.ok(ele, 'sidebar element was added');
+
+ let oldEle = ele;
+ sidebar.once('attach', function() {
+ assert.pass('attach event fired');
+
+ sidebar.once('show', function() {
+ assert.pass('show event fired');
+
+ sidebar.once('show', function() {
+ let window = getMostRecentBrowserWindow();
+ assert.notEqual(startWindow, window, 'window is new');
+
+ let sb = window.document.getElementById('sidebar');
+ if (sb && sb.docShell && sb.contentDocument && sb.contentDocument.getElementById('web-panels-browser')) {
+ end();
+ }
+ else {
+ sb.addEventListener('DOMWindowCreated', end, false);
+ }
+
+ function end() {
+ sb.removeEventListener('DOMWindowCreated', end, false);
+ let webPanelBrowser = sb.contentDocument.getElementById('web-panels-browser');
+
+ let ele = window.document.getElementById(makeID(testName));
+
+ assert.ok(ele, 'sidebar element was added 2');
+ assert.ok(isChecked(ele), 'the sidebar is checked');
+ assert.notEqual(ele, oldEle, 'there are two different sidebars');
+
+ assert.equal(isShowing(sidebar), true, 'the sidebar is showing in new window');
+
+
+ sidebar.destroy();
+
+ assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+ assert.ok(!isSidebarShowing(window), 'sidebar in most recent window is not showing');
+ assert.ok(!isSidebarShowing(startWindow), 'sidebar in most start window is not showing');
+ assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+ assert.ok(!startWindow.document.getElementById(makeID(testName)), 'sidebar id DNE');
+
+ setTimeout(function() {
+ close(window).then(done, assert.fail);
+ });
+ }
+ });
+
+ startWindow.OpenBrowserWindow();
+ });
+ });
+
+ show(sidebar);
+ assert.pass('showing the sidebar');
+}
+
+// TODO: determine if this is acceptable..
+/*
+exports.testAddonGlobalSimple = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testAddonGlobalSimple';
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: data.url('test-sidebar-addon-global.html')
+ });
+
+ sidebar.on('show', function({worker}) {
+ assert.pass('sidebar was attached');
+ assert.ok(!!worker, 'attach event has worker');
+
+ worker.port.on('X', function(msg) {
+ assert.equal(msg, '23', 'the final message is correct');
+
+ sidebar.destroy();
+
+ done();
+ });
+ worker.port.emit('X', '2');
+ });
+ show(sidebar);
+}
+*/
+
+exports.testAddonGlobalComplex = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testAddonGlobalComplex';
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: data.url('test-sidebar-addon-global.html')
+ });
+
+ sidebar.on('attach', function(worker) {
+ assert.pass('sidebar was attached');
+ assert.ok(!!worker, 'attach event has worker');
+
+ worker.port.once('Y', function(msg) {
+ assert.equal(msg, '1', 'got event from worker');
+
+ worker.port.on('X', function(msg) {
+ assert.equal(msg, '123', 'the final message is correct');
+
+ sidebar.destroy();
+
+ done();
+ });
+ worker.port.emit('X', msg + '2');
+ })
+ });
+
+ show(sidebar);
+}
+
+exports.testShowingOneSidebarAfterAnother = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testShowingOneSidebarAfterAnother';
+
+ let sidebar1 = Sidebar({
+ id: testName + '1',
+ title: testName + '1',
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+ testName + 1
+ });
+ let sidebar2 = Sidebar({
+ id: testName + '2',
+ title: testName + '2',
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+ testName + 2
+ });
+
+ let window = getMostRecentBrowserWindow();
+ let IDs = [ sidebar1.id, sidebar2.id ];
+
+ let extraMenuitems = getExtraSidebarMenuitems(window);
+ assert.equal(extraMenuitems.length, 2, 'there are two extra sidebar menuitems');
+
+ function testShowing(sb1, sb2, sbEle) {
+ assert.equal(isShowing(sidebar1), sb1);
+ assert.equal(isShowing(sidebar2), sb2);
+ assert.equal(isSidebarShowing(window), sbEle);
+ }
+ testShowing(false, false, false);
+
+ sidebar1.once('show', function() {
+ testShowing(true, false, true);
+ for each (let mi in getExtraSidebarMenuitems(window)) {
+ let menuitemID = mi.getAttribute('id').replace(/^jetpack-sidebar-/, '');
+ assert.ok(IDs.indexOf(menuitemID) >= 0, 'the extra menuitem is for one of our test sidebars');
+ assert.equal(isChecked(mi), menuitemID == sidebar1.id, 'the test sidebar menuitem has the correct checked value');
+ }
+
+ sidebar2.once('show', function() {
+ testShowing(false, true, true);
+ for each (let mi in getExtraSidebarMenuitems(window)) {
+ let menuitemID = mi.getAttribute('id').replace(/^jetpack-sidebar-/, '');
+ assert.ok(IDs.indexOf(menuitemID) >= 0, 'the extra menuitem is for one of our test sidebars');
+ assert.equal(isChecked(mi), menuitemID == sidebar2.id, 'the test sidebar menuitem has the correct checked value');
+ }
+
+ sidebar1.destroy();
+ sidebar2.destroy();
+
+ testShowing(false, false, false);
+
+ done();
+ });
+
+ show(sidebar2);
+ assert.pass('showing sidebar 2');
+ })
+ show(sidebar1);
+ assert.pass('showing sidebar 1');
+}
+
+exports.testSidebarUnload = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testSidebarUnload';
+ let loader = Loader(module);
+
+ let window = getMostRecentBrowserWindow();
+
+ assert.equal(isPrivate(window), false, 'the current window is not private');
+
+ // EXPLICIT: testing require('sdk/ui')
+ let sidebar = loader.require('sdk/ui').Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+ testName,
+ onShow: function() {
+ assert.pass('onShow works for Sidebar');
+ loader.unload();
+
+ let sidebarMI = getSidebarMenuitems();
+ for each (let mi in sidebarMI) {
+ assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+ assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
+ }
+ assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+ assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+ done();
+ }
+ })
+
+ sidebar.show();
+ assert.pass('showing the sidebar');
+}
+
+exports.testRemoteContent = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testRemoteContent';
+ try {
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'http://dne.xyz.mozilla.org'
+ });
+ assert.fail('a bad sidebar was created..');
+ sidebar.destroy();
+ }
+ catch(e) {
+ assert.ok(/The option "url" must be a valid URI./.test(e), 'remote content is not acceptable');
+ }
+}
+
+exports.testInvalidURL = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testInvalidURL';
+ try {
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'http:mozilla.org'
+ });
+ assert.fail('a bad sidebar was created..');
+ sidebar.destroy();
+ }
+ catch(e) {
+ assert.ok(/The option "url" must be a valid URI./.test(e), 'invalid URIs are not acceptable');
+ }
+}
+
+exports.testInvalidURLType = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testInvalidURLType';
+ try {
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG
+ });
+ assert.fail('a bad sidebar was created..');
+ sidebar.destroy();
+ }
+ catch(e) {
+ assert.ok(/The option "url" must be a valid URI./.test(e), 'invalid URIs are not acceptable');
+ }
+}
+
+exports.testInvalidTitle = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testInvalidTitle';
+ try {
+ let sidebar = Sidebar({
+ id: testName,
+ title: '',
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+ assert.fail('a bad sidebar was created..');
+ sidebar.destroy();
+ }
+ catch(e) {
+ assert.equal('The option "title" must be one of the following types: string', e.message, 'invalid titles are not acceptable');
+ }
+}
+
+exports.testInvalidIcon = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testInvalidIcon';
+ try {
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+ assert.fail('a bad sidebar was created..');
+ sidebar.destroy();
+ }
+ catch(e) {
+ assert.ok(/The option "icon" must be a local URL or an object with/.test(e), 'invalid icons are not acceptable');
+ }
+}
+
+exports.testInvalidID = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testInvalidID';
+ try {
+ let sidebar = Sidebar({
+ id: '!',
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+ assert.fail('a bad sidebar was created..');
+ sidebar.destroy();
+ }
+ catch(e) {
+ assert.ok(/The option "id" must be a valid alphanumeric id/.test(e), 'invalid ids are not acceptable');
+ }
+}
+
+exports.testInvalidBlankID = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testInvalidBlankID';
+ try {
+ let sidebar = Sidebar({
+ id: '',
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+ assert.fail('a bad sidebar was created..');
+ sidebar.destroy();
+ }
+ catch(e) {
+ assert.ok(/The option "id" must be a valid alphanumeric id/.test(e), 'invalid ids are not acceptable');
+ }
+}
+
+exports.testInvalidNullID = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testInvalidNullID';
+ try {
+ let sidebar = Sidebar({
+ id: null,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+ assert.fail('a bad sidebar was created..');
+ sidebar.destroy();
+ }
+ catch(e) {
+ assert.ok(/The option "id" must be a valid alphanumeric id/.test(e), 'invalid ids are not acceptable');
+ }
+}
+
+exports.testInvalidUndefinedID = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testInvalidBlankID';
+ try {
+ let sidebar = Sidebar({
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+ assert.fail('a bad sidebar was created..');
+ sidebar.destroy();
+ }
+ catch(e) {
+ assert.ok(/The option "id" must be a valid alphanumeric id/.test(e), 'invalid ids are not acceptable');
+ }
+}
+
+// TEST: edge case where web panel is destroyed while loading
+exports.testDestroyEdgeCaseBug = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testDestroyEdgeCaseBug';
+ let window = getMostRecentBrowserWindow();
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+
+ // NOTE: purposely not listening to show event b/c the event happens
+ // between now and then.
+ sidebar.show();
+
+ assert.equal(isPrivate(window), false, 'the new window is not private');
+ assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+
+ //assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+ open().then(focus).then(function(window2) {
+ assert.equal(isPrivate(window2), false, 'the new window is not private');
+ assert.equal(isSidebarShowing(window2), false, 'the sidebar is not showing');
+ assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+
+ sidebar.destroy();
+ assert.pass('destroying the sidebar');
+
+ close(window2).then(function() {
+ let loader = Loader(module);
+
+ assert.equal(isPrivate(window), false, 'the current window is not private');
+
+ let sidebar = loader.require('sdk/ui/sidebar').Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+ testName,
+ onShow: function() {
+ assert.pass('onShow works for Sidebar');
+ loader.unload();
+
+ let sidebarMI = getSidebarMenuitems();
+ for each (let mi in sidebarMI) {
+ assert.ok(BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) >= 0, 'the menuitem is for a built-in sidebar')
+ assert.ok(!isChecked(mi), 'no sidebar menuitem is checked');
+ }
+ assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
+ assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+ done();
+ }
+ })
+
+ sidebar.show();
+ assert.pass('showing the sidebar');
+
+ });
+ });
+}
+
+exports.testClickingACheckedMenuitem = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testClickingACheckedMenuitem';
+ let window = getMostRecentBrowserWindow();
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName,
+ });
+
+ sidebar.show().then(function() {
+ assert.pass('the show callback works');
+
+ sidebar.once('hide', function() {
+ assert.pass('clicking the menuitem after the sidebar has shown hides it.');
+ sidebar.destroy();
+ done();
+ });
+
+ let menuitem = window.document.getElementById(makeID(sidebar.id));
+ simulateCommand(menuitem);
+ });
+};
+
+exports.testClickingACheckedButton = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testClickingACheckedButton';
+ let window = getMostRecentBrowserWindow();
+
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName,
+ onShow: function onShow() {
+ sidebar.off('show', onShow);
+
+ assert.pass('the sidebar was shown');
+ //assert.equal(button.checked, true, 'the button is now checked');
+
+ sidebar.once('hide', function() {
+ assert.pass('clicking the button after the sidebar has shown hides it.');
+
+ sidebar.once('show', function() {
+ assert.pass('clicking the button again shows it.');
+
+ sidebar.hide().then(function() {
+ assert.pass('hide callback works');
+ assert.equal(isShowing(sidebar), false, 'the sidebar is not showing, final.');
+
+ assert.pass('the sidebar was destroying');
+ sidebar.destroy();
+ assert.pass('the sidebar was destroyed');
+
+ assert.equal(button.parentNode, null, 'the button\'s parents were shot')
+
+ done();
+ }, assert.fail);
+ });
+
+ assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+
+ // TODO: figure out why this is necessary..
+ setTimeout(function() simulateCommand(button));
+ });
+
+ assert.equal(isShowing(sidebar), true, 'the sidebar is showing');
+
+ simulateCommand(button);
+ }
+ });
+
+ let { node: button } = getWidget(sidebar.id, window);
+ //assert.equal(button.checked, false, 'the button exists and is not checked');
+
+ assert.equal(isShowing(sidebar), false, 'the sidebar is not showing');
+ simulateCommand(button);
+}
+
+exports.testTitleSetter = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testTitleSetter';
+ let { document } = getMostRecentBrowserWindow();
+
+ let sidebar1 = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName,
+ });
+
+ assert.equal(sidebar1.title, testName, 'title getter works');
+
+ sidebar1.show().then(function() {
+ let button = document.querySelector('toolbarbutton[label=' + testName + ']');
+ assert.ok(button, 'button was found');
+
+ assert.equal(document.getElementById(makeID(sidebar1.id)).getAttribute('label'),
+ testName,
+ 'the menuitem label is correct');
+
+ assert.equal(document.getElementById('sidebar-title').value, testName, 'the menuitem label is correct');
+
+ sidebar1.title = 'foo';
+
+ assert.equal(sidebar1.title, 'foo', 'title getter works');
+
+ assert.equal(document.getElementById(makeID(sidebar1.id)).getAttribute('label'),
+ 'foo',
+ 'the menuitem label was updated');
+
+ assert.equal(document.getElementById('sidebar-title').value, 'foo', 'the sidebar title was updated');
+
+ assert.equal(button.getAttribute('label'), 'foo', 'the button label was updated');
+
+ sidebar1.destroy();
+ done();
+ }, assert.fail);
+}
+
+exports.testURLSetter = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testURLSetter';
+ let window = getMostRecentBrowserWindow();
+ let { document } = window;
+ let url = 'data:text/html;charset=utf-8,'+testName;
+
+ let sidebar1 = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ assert.equal(sidebar1.url, url, 'url getter works');
+ assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+ assert.ok(!isChecked(document.getElementById(makeID(sidebar1.id))),
+ 'the menuitem is not checked');
+ assert.equal(isSidebarShowing(window), false, 'the new window sidebar is not showing');
+
+ windowPromise(window.OpenBrowserWindow(), 'load').then(function(window) {
+ let { document } = window;
+ assert.pass('new window was opened');
+
+ sidebar1.show().then(function() {
+ assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+ assert.ok(isChecked(document.getElementById(makeID(sidebar1.id))),
+ 'the menuitem is checked');
+ assert.ok(isSidebarShowing(window), 'the new window sidebar is showing');
+
+ sidebar1.once('show', function() {
+ assert.pass('setting the sidebar.url causes a show event');
+
+ assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+ assert.ok(isSidebarShowing(window), 'the new window sidebar is still showing');
+
+ assert.ok(isChecked(document.getElementById(makeID(sidebar1.id))),
+ 'the menuitem is still checked');
+
+ sidebar1.destroy();
+
+ close(window).then(done);
+ });
+
+ sidebar1.url = (url + '1');
+
+ assert.equal(sidebar1.url, (url + '1'), 'url getter works');
+ assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+ assert.ok(isSidebarShowing(window), 'the new window sidebar is showing');
+ }, assert.fail);
+ }, assert.fail);
+}
+
+exports.testDuplicateID = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testDuplicateID';
+ let window = getMostRecentBrowserWindow();
+ let { document } = window;
+ let url = 'data:text/html;charset=utf-8,'+testName;
+
+ let sidebar1 = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ assert.throws(function() {
+ Sidebar({
+ id: testName,
+ title: testName + 1,
+ icon: BLANK_IMG,
+ url: url + 2
+ }).destroy();
+ }, /The ID .+ seems already used\./i, 'duplicate IDs will throw errors');
+
+ sidebar1.destroy();
+}
+
+exports.testURLSetterToSameValueReloadsSidebar = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testURLSetterToSameValueReloadsSidebar';
+ let window = getMostRecentBrowserWindow();
+ let { document } = window;
+ let url = 'data:text/html;charset=utf-8,'+testName;
+
+ let sidebar1 = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ assert.equal(sidebar1.url, url, 'url getter works');
+ assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+ assert.ok(!isChecked(document.getElementById(makeID(sidebar1.id))),
+ 'the menuitem is not checked');
+ assert.equal(isSidebarShowing(window), false, 'the new window sidebar is not showing');
+
+ windowPromise(window.OpenBrowserWindow(), 'load').then(function(window) {
+ let { document } = window;
+ assert.pass('new window was opened');
+
+ sidebar1.show().then(function() {
+ assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+ assert.ok(isChecked(document.getElementById(makeID(sidebar1.id))),
+ 'the menuitem is checked');
+ assert.ok(isSidebarShowing(window), 'the new window sidebar is showing');
+
+ sidebar1.once('show', function() {
+ assert.pass('setting the sidebar.url causes a show event');
+
+ assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+ assert.ok(isSidebarShowing(window), 'the new window sidebar is still showing');
+
+ assert.ok(isChecked(document.getElementById(makeID(sidebar1.id))),
+ 'the menuitem is still checked');
+
+ sidebar1.destroy();
+
+ close(window).then(done);
+ });
+
+ sidebar1.url = url;
+
+ assert.equal(sidebar1.url, url, 'url getter works');
+ assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+ assert.ok(isSidebarShowing(window), 'the new window sidebar is showing');
+ }, assert.fail);
+ }, assert.fail);
+}
+
+exports.testButtonShowingInOneWindowDoesNotAffectOtherWindows = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testButtonShowingInOneWindowDoesNotAffectOtherWindows';
+ let window1 = getMostRecentBrowserWindow();
+ let url = 'data:text/html;charset=utf-8,'+testName;
+
+ let sidebar1 = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ assert.equal(sidebar1.url, url, 'url getter works');
+ assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+ let checkCount = 1;
+ function checkSidebarShowing(window, expected) {
+ assert.pass('check count ' + checkCount++);
+
+ let mi = window.document.getElementById(makeID(sidebar1.id));
+ if (mi) {
+ assert.equal(isChecked(mi), expected,
+ 'the menuitem is not checked');
+ }
+ assert.equal(isSidebarShowing(window), expected || false, 'the new window sidebar is not showing');
+ }
+ checkSidebarShowing(window1, false);
+
+ windowPromise(window1.OpenBrowserWindow(), 'load').then(function(window) {
+ let { document } = window;
+ assert.pass('new window was opened!');
+
+ // waiting for show using button
+ sidebar1.once('show', function() {
+ // check state of the new window
+ assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+ checkSidebarShowing(window, true);
+
+ // check state of old window
+ checkSidebarShowing(window1, false);
+
+ // waiting for show using url setter
+ sidebar1.once('show', function() {
+ assert.pass('setting the sidebar.url causes a new show event');
+
+ // check state of the new window
+ assert.equal(isShowing(sidebar1), true, 'the sidebar is showing');
+ checkSidebarShowing(window, true);
+
+ // check state of old window
+ checkSidebarShowing(window1, false);
+
+ // calling destroy() twice should not matter
+ sidebar1.destroy();
+ sidebar1.destroy();
+
+ // check state of the new window
+ assert.equal(isShowing(sidebar1), false, 'the sidebar is not showing');
+ checkSidebarShowing(window, undefined);
+
+ // check state of old window
+ checkSidebarShowing(window1, undefined);
+
+ close(window).then(done);
+ });
+
+ assert.pass('setting sidebar1.url');
+ sidebar1.url += '1';
+ assert.pass('set sidebar1.url');
+ });
+
+ // clicking the sidebar button on the second window
+ let { node: button } = getWidget(sidebar1.id, window);
+ assert.ok(!!button, 'the button was found!');
+ simulateCommand(button);
+
+ }, assert.fail);
+}
+
+exports.testHidingAHiddenSidebarRejects = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testHidingAHiddenSidebarRejects';
+ let url = 'data:text/html;charset=utf-8,'+testName;
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ sidebar.hide().then(assert.fail, assert.pass).then(function() {
+ sidebar.destroy();
+ done();
+ }, assert.fail);
+}
+
+exports.testGCdSidebarsOnUnload = function(assert, done) {
+ const loader = Loader(module);
+ const { Sidebar } = loader.require('sdk/ui/sidebar');
+ const window = getMostRecentBrowserWindow();
+
+ let testName = 'testGCdSidebarsOnUnload';
+ let url = 'data:text/html;charset=utf-8,'+testName;
+
+ assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+ // IMPORTANT: make no reference to the sidebar instance, so it is GC'd
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ sidebar.show().then(function() {
+ sidebar = null;
+
+ assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+
+ let buttonID = getWidget(testName, window).node.getAttribute('id');
+ let menuitemID = makeID(testName);
+
+ assert.ok(!!window.document.getElementById(buttonID), 'the button was found');
+ assert.ok(!!window.document.getElementById(menuitemID), 'the menuitem was found');
+
+ Cu.schedulePreciseGC(function() {
+ loader.unload();
+
+ assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing after unload');
+ assert.ok(!window.document.getElementById(buttonID), 'the button was removed');
+ assert.ok(!window.document.getElementById(menuitemID), 'the menuitem was removed');
+
+ done();
+ })
+ }, assert.fail).then(null, assert.fail);
+}
+
+exports.testGCdShowingSidebarsOnUnload = function(assert, done) {
+ const loader = Loader(module);
+ const { Sidebar } = loader.require('sdk/ui/sidebar');
+ const window = getMostRecentBrowserWindow();
+
+ let testName = 'testGCdShowingSidebarsOnUnload';
+ let url = 'data:text/html;charset=utf-8,'+testName;
+
+ assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ sidebar.on('show', function() {
+ sidebar = null;
+
+ assert.equal(isSidebarShowing(window), true, 'the sidebar is showing');
+
+ let buttonID = getWidget(testName, window).node.getAttribute('id');
+ let menuitemID = makeID(testName);
+
+ assert.ok(!!window.document.getElementById(buttonID), 'the button was found');
+ assert.ok(!!window.document.getElementById(menuitemID), 'the menuitem was found');
+
+ Cu.schedulePreciseGC(function() {
+ assert.equal(isSidebarShowing(window), true, 'the sidebar is still showing after gc');
+ assert.ok(!!window.document.getElementById(buttonID), 'the button was found after gc');
+ assert.ok(!!window.document.getElementById(menuitemID), 'the menuitem was found after gc');
+
+ loader.unload();
+
+ assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing after unload');
+ assert.ok(!window.document.getElementById(buttonID), 'the button was removed');
+ assert.ok(!window.document.getElementById(menuitemID), 'the menuitem was removed');
+
+ done();
+ })
+ });
+
+ sidebar.show();
+}
+
+exports.testGCdHiddenSidebarsOnUnload = function(assert, done) {
+ const loader = Loader(module);
+ const { Sidebar } = loader.require('sdk/ui/sidebar');
+ const window = getMostRecentBrowserWindow();
+
+ let testName = 'testGCdHiddenSidebarsOnUnload';
+ let url = 'data:text/html;charset=utf-8,'+testName;
+
+ assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
+
+ // IMPORTANT: make no reference to the sidebar instance, so it is GC'd
+ Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ let buttonID = getWidget(testName, window).node.getAttribute('id');
+ let menuitemID = makeID(testName);
+
+ assert.ok(!!window.document.getElementById(buttonID), 'the button was found');
+ assert.ok(!!window.document.getElementById(menuitemID), 'the menuitem was found');
+
+ Cu.schedulePreciseGC(function() {
+ assert.ok(!!window.document.getElementById(buttonID), 'the button was found after gc');
+ assert.ok(!!window.document.getElementById(menuitemID), 'the menuitem was found after gc');
+
+ loader.unload();
+
+ assert.ok(!window.document.getElementById(buttonID), 'the button was removed');
+ assert.ok(!window.document.getElementById(menuitemID), 'the menuitem was removed');
+
+ done();
+ });
+}
+
+exports.testSidebarGettersAndSettersAfterDestroy = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testSidebarGettersAndSettersAfterDestroy';
+ let url = 'data:text/html;charset=utf-8,'+testName;
+
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ sidebar.destroy();
+
+ assert.equal(sidebar.id, undefined, 'sidebar after destroy has no id');
+
+ assert.throws(() => sidebar.id = 'foo-tang',
+ /^setting a property that has only a getter/,
+ 'id cannot be set at runtime');
+
+ assert.equal(sidebar.id, undefined, 'sidebar after destroy has no id');
+
+ assert.equal(sidebar.title, undefined, 'sidebar after destroy has no title');
+ sidebar.title = 'boo-tang';
+ assert.equal(sidebar.title, undefined, 'sidebar after destroy has no title');
+
+ assert.equal(sidebar.url, undefined, 'sidebar after destroy has no url');
+ sidebar.url = url + 'barz';
+ assert.equal(sidebar.url, undefined, 'sidebar after destroy has no url');
+}
+
+exports.testButtonIconSet = function(assert) {
+ const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
+ let loader = Loader(module);
+ let { Sidebar } = loader.require('sdk/ui');
+ let testName = 'testButtonIconSet';
+ let url = 'data:text/html;charset=utf-8,'+testName;
+
+ // Test remote icon set
+ assert.throws(
+ () => Sidebar({
+ id: 'my-button-10',
+ title: 'my button',
+ url: url,
+ icon: {
+ '16': 'http://www.mozilla.org/favicon.ico'
+ }
+ }),
+ /^The option "icon"/,
+ 'throws on no valid icon given');
+
+ let sidebar = Sidebar({
+ id: 'my-button-11',
+ title: 'my button',
+ url: url,
+ icon: {
+ '16': './icon16.png',
+ '32': './icon32.png',
+ '64': './icon64.png'
+ }
+ });
+
+ let { node, id: widgetId } = getWidget(sidebar.id);
+ let { devicePixelRatio } = node.ownerDocument.defaultView;
+
+ let size = 16 * devicePixelRatio;
+
+ assert.equal(node.getAttribute('image'), data.url(sidebar.icon[size].substr(2)),
+ 'the icon is set properly in navbar');
+
+ let size = 32 * devicePixelRatio;
+
+ CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_PANEL);
+
+ assert.equal(node.getAttribute('image'), data.url(sidebar.icon[size].substr(2)),
+ 'the icon is set properly in panel');
+
+ // Using `loader.unload` without move back the button to the original area
+ // raises an error in the CustomizableUI. This is doesn't happen if the
+ // button is moved manually from navbar to panel. I believe it has to do
+ // with `addWidgetToArea` method, because even with a `timeout` the issue
+ // persist.
+ CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR);
+
+ loader.unload();
+}
+
+exports.testSidebarLeakCheckDestroyAfterAttach = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testSidebarLeakCheckDestroyAfterAttach';
+ let window = getMostRecentBrowserWindow();
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+
+ sidebar.on('attach', function() {
+ assert.pass('the sidebar was shown');
+
+ sidebar.on('show', function() {
+ assert.fail('the sidebar show listener should have been removed');
+ });
+ assert.pass('added a sidebar show listener');
+
+ sidebar.on('hide', function() {
+ assert.fail('the sidebar hide listener should have been removed');
+ });
+ assert.pass('added a sidebar hide listener');
+
+ let panelBrowser = window.document.getElementById('sidebar').contentDocument.getElementById('web-panels-browser');
+ panelBrowser.contentWindow.addEventListener('unload', function onUnload() {
+ panelBrowser.contentWindow.removeEventListener('unload', onUnload, false);
+ // wait a tick..
+ setTimeout(function() {
+ assert.pass('the sidebar web panel was unloaded properly');
+ done();
+ })
+ }, false);
+
+ sidebar.destroy();
+ });
+
+ assert.pass('showing the sidebar');
+ sidebar.show();
+}
+
+exports.testSidebarLeakCheckUnloadAfterAttach = function(assert, done) {
+ const loader = Loader(module);
+ const { Sidebar } = loader.require('sdk/ui/sidebar');
+ let testName = 'testSidebarLeakCheckUnloadAfterAttach';
+ let window = getMostRecentBrowserWindow();
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,'+testName
+ });
+
+ sidebar.on('attach', function() {
+ assert.pass('the sidebar was shown');
+
+ sidebar.on('show', function() {
+ assert.fail('the sidebar show listener should have been removed');
+ });
+ assert.pass('added a sidebar show listener');
+
+ sidebar.on('hide', function() {
+ assert.fail('the sidebar hide listener should have been removed');
+ });
+ assert.pass('added a sidebar hide listener');
+
+ let panelBrowser = window.document.getElementById('sidebar').contentDocument.getElementById('web-panels-browser');
+ panelBrowser.contentWindow.addEventListener('unload', function onUnload() {
+ panelBrowser.contentWindow.removeEventListener('unload', onUnload, false);
+ // wait a tick..
+ setTimeout(function() {
+ assert.pass('the sidebar web panel was unloaded properly');
+ done();
+ })
+ }, false);
+
+ loader.unload();
+ });
+
+ assert.pass('showing the sidebar');
+ sidebar.show();
+}
+
+exports.testTwoSidebarsWithSameTitleAndURL = function(assert) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testTwoSidebarsWithSameTitleAndURL';
+
+ let title = testName;
+ let url = 'data:text/html;charset=utf-8,' + testName;
+
+ let sidebar1 = Sidebar({
+ id: testName + 1,
+ title: title,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ assert.throws(function() {
+ Sidebar({
+ id: testName + 2,
+ title: title,
+ icon: BLANK_IMG,
+ url: url
+ }).destroy();
+ }, /title.+url.+invalid/i, 'Creating two sidebars with the same title + url is not allowed');
+
+ let sidebar2 = Sidebar({
+ id: testName + 2,
+ title: title,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,X'
+ });
+
+ assert.throws(function() {
+ sidebar2.url = url;
+ }, /title.+url.+invalid/i, 'Creating two sidebars with the same title + url is not allowed');
+
+ sidebar2.title = 'foo';
+ sidebar2.url = url;
+
+ assert.throws(function() {
+ sidebar2.title = title;
+ }, /title.+url.+invalid/i, 'Creating two sidebars with the same title + url is not allowed');
+
+ sidebar1.destroy();
+ sidebar2.destroy();
+}
+
+exports.testButtonToOpenXToClose = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testButtonToOpenXToClose';
+
+ let title = testName;
+ let url = 'data:text/html;charset=utf-8,' + testName;
+ let window = getMostRecentBrowserWindow();
+
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url,
+ onShow: function() {
+ assert.ok(isChecked(button), 'button is checked');
+ assert.ok(isChecked(menuitem), 'menuitem is checked');
+
+ let closeButton = window.document.querySelector('#sidebar-header > toolbarbutton.tabs-closebutton');
+ simulateCommand(closeButton);
+ },
+ onHide: function() {
+ assert.ok(!isChecked(button), 'button is not checked');
+ assert.ok(!isChecked(menuitem), 'menuitem is not checked');
+
+ sidebar.destroy();
+ done();
+ }
+ });
+
+ let { node: button } = getWidget(sidebar.id, window);
+ let menuitem = window.document.getElementById(makeID(sidebar.id));
+
+ assert.ok(!isChecked(button), 'button is not checked');
+ assert.ok(!isChecked(menuitem), 'menuitem is not checked');
+
+ simulateCommand(button);
+}
+
+exports.testButtonToOpenMenuitemToClose = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testButtonToOpenMenuitemToClose';
+
+ let title = testName;
+ let url = 'data:text/html;charset=utf-8,' + testName;
+ let window = getMostRecentBrowserWindow();
+
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url,
+ onShow: function() {
+ assert.ok(isChecked(button), 'button is checked');
+ assert.ok(isChecked(menuitem), 'menuitem is checked');
+
+ simulateCommand(menuitem);
+ },
+ onHide: function() {
+ assert.ok(!isChecked(button), 'button is not checked');
+ assert.ok(!isChecked(menuitem), 'menuitem is not checked');
+
+ sidebar.destroy();
+ done();
+ }
+ });
+
+ let { node: button } = getWidget(sidebar.id, window);
+ let menuitem = window.document.getElementById(makeID(sidebar.id));
+
+ assert.ok(!isChecked(button), 'button is not checked');
+ assert.ok(!isChecked(menuitem), 'menuitem is not checked');
+
+ simulateCommand(button);
+}
+
+exports.testDestroyWhileNonBrowserWindowIsOpen = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testDestroyWhileNonBrowserWindowIsOpen';
+ let url = 'data:text/html;charset=utf-8,' + testName;
+
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: url
+ });
+
+ open('chrome://browser/content/preferences/preferences.xul').then(function(window) {
+ try {
+ sidebar.show();
+ assert.equal(isSidebarShowing(getMostRecentBrowserWindow()), true, 'the sidebar is showing');
+
+ sidebar.destroy();
+
+ assert.pass('sidebar was destroyed while a non browser window was open');
+ }
+ catch(e) {
+ assert.fail(e);
+ }
+
+ return window;
+ }).then(close).then(function() {
+ assert.equal(isSidebarShowing(getMostRecentBrowserWindow()), false, 'the sidebar is not showing');
+ }).then(done, assert.fail);
+}
+
+exports.testEventListeners = function(assert, done) {
+ const { Sidebar } = require('sdk/ui/sidebar');
+ let testName = 'testWhatThisIsInSidebarEventListeners';
+ let eventListenerOrder = [];
+
+ let constructorOnShow = defer();
+ let constructorOnHide = defer();
+ let constructorOnAttach = defer();
+
+ let onShow = defer();
+ let onHide = defer();
+ let onAttach = defer();
+
+ let onceShow = defer();
+ let onceHide = defer();
+ let onceAttach = defer();
+
+ function testThis() {
+ assert(this, sidebar, '`this` is correct');
+ }
+
+ let sidebar = Sidebar({
+ id: testName,
+ title: testName,
+ icon: BLANK_IMG,
+ url: 'data:text/html;charset=utf-8,' + testName,
+ onShow: function() {
+ assert.equal(this, sidebar, '`this` is correct in onShow');
+ eventListenerOrder.push('onShow');
+ constructorOnShow.resolve();
+ },
+ onAttach: function() {
+ assert.equal(this, sidebar, '`this` is correct in onAttach');
+ eventListenerOrder.push('onAttach');
+ constructorOnAttach.resolve();
+ },
+ onHide: function() {
+ assert.equal(this, sidebar, '`this` is correct in onHide');
+ eventListenerOrder.push('onHide');
+ constructorOnHide.resolve();
+ }
+ });
+
+ sidebar.once('show', function() {
+ assert.equal(this, sidebar, '`this` is correct in once show');
+ eventListenerOrder.push('once show');
+ onceShow.resolve();
+ });
+ sidebar.once('attach', function() {
+ assert.equal(this, sidebar, '`this` is correct in once attach');
+ eventListenerOrder.push('once attach');
+ onceAttach.resolve();
+ });
+ sidebar.once('hide', function() {
+ assert.equal(this, sidebar, '`this` is correct in once hide');
+ eventListenerOrder.push('once hide');
+ onceHide.resolve();
+ });
+
+ sidebar.on('show', function() {
+ assert.equal(this, sidebar, '`this` is correct in on show');
+ eventListenerOrder.push('on show');
+ onShow.resolve();
+
+ sidebar.hide();
+ });
+ sidebar.on('attach', function() {
+ assert.equal(this, sidebar, '`this` is correct in on attach');
+ eventListenerOrder.push('on attach');
+ onAttach.resolve();
+ });
+ sidebar.on('hide', function() {
+ assert.equal(this, sidebar, '`this` is correct in on hide');
+ eventListenerOrder.push('on hide');
+ onHide.resolve();
+ });
+
+ all(constructorOnShow.promise,
+ constructorOnAttach.promise,
+ constructorOnHide.promise,
+ onceShow.promise,
+ onceAttach.promise,
+ onceHide.promise,
+ onShow.promise,
+ onAttach.promise,
+ onHide.promise).then(function() {
+ assert.equal(eventListenerOrder.join(), [
+ 'onAttach',
+ 'once attach',
+ 'on attach',
+ 'onShow',
+ 'once show',
+ 'on show',
+ 'onHide',
+ 'once hide',
+ 'on hide'
+ ].join(), 'the event order was correct');
+ sidebar.destroy();
+ }).then(done, assert.fail);
+
+ sidebar.show();
+}
+
+// If the module doesn't support the app we're being run in, require() will
+// throw. In that case, remove all tests above from exports, and add one dummy
+// test that passes.
+try {
+ require('sdk/ui/sidebar');
+}
+catch (err) {
+ if (!/^Unsupported Application/.test(err.message))
+ throw err;
+
+ module.exports = {
+ 'test Unsupported Application': assert => assert.pass(err.message)
+ }
+}
+
+require('sdk/test').run(exports);
diff --git a/addon-sdk/source/test/test-window-observer.js b/addon-sdk/source/test/test-window-observer.js
index 2c27bfce76f7..ddd5537dc26e 100644
--- a/addon-sdk/source/test/test-window-observer.js
+++ b/addon-sdk/source/test/test-window-observer.js
@@ -3,17 +3,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-// Opening new windows in Fennec causes issues
-module.metadata = {
- engines: {
- 'Firefox': '*'
- }
-};
-
const { Loader } = require("sdk/test/loader");
const { open, close } = require("sdk/window/helpers");
const { browserWindows: windows } = require("sdk/windows");
const { isBrowser } = require('sdk/window/utils');
+const app = require("sdk/system/xul-app");
exports["test unload window observer"] = function(assert, done) {
// Hacky way to be able to create unloadable modules via makeSandboxedLoader.
@@ -23,17 +17,14 @@ exports["test unload window observer"] = function(assert, done) {
let closed = 0;
let windowsOpen = windows.length;
- observer.on("open", function onOpen(window) {
- // Ignoring non-browser windows
- if (isBrowser(window))
- opened++;
- });
- observer.on("close", function onClose(window) {
- // Ignore non-browser windows & already opened `activeWindow` (unload will
- // emit close on it even though it is not actually closed).
- if (isBrowser(window))
- closed++;
- });
+ observer.on("open", onOpen);
+ observer.on("close", onClose);
+
+ // On Fennec, only test that the module does not throw an error
+ if (app.is("Fennec")) {
+ assert.pass("Windows observer did not throw on Fennec");
+ return cleanUp();
+ }
// Open window and close it to trigger observers.
open().
@@ -46,7 +37,25 @@ exports["test unload window observer"] = function(assert, done) {
assert.equal(1, opened, "observer open was called before unload only");
assert.equal(windowsOpen + 1, closed, "observer close was called before unload only");
}).
- then(done, assert.fail);
+ then(cleanUp, assert.fail);
+
+ function cleanUp () {
+ observer.removeListener("open", onOpen);
+ observer.removeListener("close", onClose);
+ done();
+ }
+
+ function onOpen(window) {
+ // Ignoring non-browser windows
+ if (isBrowser(window))
+ opened++;
+ }
+ function onClose(window) {
+ // Ignore non-browser windows & already opened `activeWindow` (unload will
+ // emit close on it even though it is not actually closed).
+ if (isBrowser(window))
+ closed++;
+ }
};
require("test").run(exports);
From 12e2a99b72368d602f87dfd569b72cd2daf03ee8 Mon Sep 17 00:00:00 2001
From: Nick Fitzgerald
Date: Fri, 16 Aug 2013 14:59:04 -0700
Subject: [PATCH 27/46] Bug 901712 - black boxing doesn't work with source
maps; r=dcamp
---
toolkit/devtools/server/actors/script.js | 312 +++++++++++++++---
.../devtools/server/tests/unit/head_dbg.js | 7 +
.../server/tests/unit/test_blackboxing-06.js | 104 ++++++
.../server/tests/unit/test_nesting-01.js | 38 +++
.../server/tests/unit/test_nesting-02.js | 68 ++++
.../server/tests/unit/test_sourcemaps-02.js | 2 -
.../devtools/server/tests/unit/xpcshell.ini | 3 +
7 files changed, 478 insertions(+), 56 deletions(-)
create mode 100644 toolkit/devtools/server/tests/unit/test_blackboxing-06.js
create mode 100644 toolkit/devtools/server/tests/unit/test_nesting-01.js
create mode 100644 toolkit/devtools/server/tests/unit/test_nesting-02.js
diff --git a/toolkit/devtools/server/actors/script.js b/toolkit/devtools/server/actors/script.js
index 29c4757d01c3..0148597024b1 100644
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -131,9 +131,7 @@ BreakpointStore.prototype = {
},
/**
- * Checks if the breakpoint store has a requested breakpoint
- * Returns the stored breakpoint if it exists
- * null otherwise
+ * Checks if the breakpoint store has a requested breakpoint.
*
* @param Object aLocation
* The location of the breakpoint you are retrieving. It is an object
@@ -141,6 +139,7 @@ BreakpointStore.prototype = {
* - url
* - line
* - column (optional)
+ * @returns The stored breakpoint if it exists, null otherwise.
*/
hasBreakpoint: function BS_hasBreakpoint(aLocation) {
let { url, line, column } = aLocation;
@@ -179,7 +178,7 @@ BreakpointStore.prototype = {
for (let url of this._iterUrls(aSearchParams.url)) {
for (let line of this._iterLines(url, aSearchParams.line)) {
// Always yield whole line breakpoints first. See comment in
- // |BreakpointStore.prototype.getBreakpoint|.
+ // |BreakpointStore.prototype.hasBreakpoint|.
if (aSearchParams.column == null
&& this._wholeLineBreakpoints[url]
&& this._wholeLineBreakpoints[url][line]) {
@@ -255,6 +254,136 @@ BreakpointStore.prototype = {
},
};
+/**
+ * Manages pushing event loops and automatically pops and exits them in the
+ * correct order as they are resolved.
+ *
+ * @param nsIJSInspector inspector
+ * The underlying JS inspector we use to enter and exit nested event
+ * loops.
+ * @param Object hooks
+ * An object with the following properties:
+ * - url: The URL string of the debuggee we are spinning an event loop
+ * for.
+ * - preNest: function called before entering a nested event loop
+ * - postNest: function called after exiting a nested event loop
+ * @param ThreadActor thread
+ * The thread actor instance that owns this EventLoopStack.
+ */
+function EventLoopStack({ inspector, thread, hooks }) {
+ this._inspector = inspector;
+ this._hooks = hooks;
+ this._thread = thread;
+}
+
+EventLoopStack.prototype = {
+ /**
+ * The number of nested event loops on the stack.
+ */
+ get size() {
+ return this._inspector.eventLoopNestLevel;
+ },
+
+ /**
+ * The URL of the debuggee who pushed the event loop on top of the stack.
+ */
+ get lastPausedUrl() {
+ return this.size > 0
+ ? this._inspector.lastNestRequestor.url
+ : null;
+ },
+
+ /**
+ * Push a new nested event loop onto the stack.
+ *
+ * @returns EventLoop
+ */
+ push: function () {
+ return new EventLoop({
+ inspector: this._inspector,
+ thread: this._thread,
+ hooks: this._hooks
+ });
+ }
+};
+
+/**
+ * An object that represents a nested event loop. It is used as the nest
+ * requestor with nsIJSInspector instances.
+ *
+ * @param nsIJSInspector inspector
+ * The JS Inspector that runs nested event loops.
+ * @param ThreadActor thread
+ * The thread actor that is creating this nested event loop.
+ * @param Object hooks
+ * The same hooks object passed into EventLoopStack during its
+ * initialization.
+ */
+function EventLoop({ inspector, thread, hooks }) {
+ this._inspector = inspector;
+ this._thread = thread;
+ this._hooks = hooks;
+
+ this.enter = this.enter.bind(this);
+ this.resolve = this.resolve.bind(this);
+}
+
+EventLoop.prototype = {
+ entered: false,
+ resolved: false,
+ get url() { return this._hooks.url; },
+
+ /**
+ * Enter this nested event loop.
+ */
+ enter: function () {
+ let nestData = this._hooks.preNest
+ ? this._hooks.preNest()
+ : null;
+
+ this.entered = true;
+ this._inspector.enterNestedEventLoop(this);
+
+ // Keep exiting nested event loops while the last requestor is resolved.
+ if (this._inspector.eventLoopNestLevel > 0) {
+ const { resolved } = this._inspector.lastNestRequestor;
+ if (resolved) {
+ this._inspector.exitNestedEventLoop();
+ }
+ }
+
+ dbg_assert(this._thread.state === "running",
+ "Should be in the running state");
+
+ if (this._hooks.postNest) {
+ this._hooks.postNest(nestData);
+ }
+ },
+
+ /**
+ * Resolve this nested event loop.
+ *
+ * @returns boolean
+ * True if we exited this nested event loop because it was on top of
+ * the stack, false if there is another nested event loop above this
+ * one that hasn't resolved yet.
+ */
+ resolve: function () {
+ if (!this.entered) {
+ throw new Error("Can't resolve an event loop before it has been entered!");
+ }
+ if (this.resolved) {
+ throw new Error("Already resolved this nested event loop!");
+ }
+ this.resolved = true;
+ if (this === this._inspector.lastNestRequestor) {
+ this._inspector.exitNestedEventLoop();
+ return true;
+ }
+ return false;
+ },
+};
+
/**
* JSD2 actors.
*/
@@ -280,6 +409,11 @@ function ThreadActor(aHooks, aGlobal)
this._environmentActors = [];
this._hooks = aHooks;
this.global = aGlobal;
+ this._nestedEventLoops = new EventLoopStack({
+ inspector: DebuggerServer.xpcInspector,
+ hooks: aHooks,
+ thread: this
+ });
// A map of actorID -> actor for breakpoints created and managed by the server.
this._hiddenBreakpoints = new Map();
@@ -326,6 +460,27 @@ ThreadActor.prototype = {
return this._sources;
},
+ /**
+ * Keep track of all of the nested event loops we use to pause the debuggee
+ * when we hit a breakpoint/debugger statement/etc in one place so we can
+ * resolve them when we get resume packets. We have more than one (and keep
+ * them in a stack) because we can pause within client evals.
+ */
+ _threadPauseEventLoops: null,
+ _pushThreadPause: function TA__pushThreadPause() {
+ if (!this._threadPauseEventLoops) {
+ this._threadPauseEventLoops = [];
+ }
+ const eventLoop = this._nestedEventLoops.push();
+ this._threadPauseEventLoops.push(eventLoop);
+ eventLoop.enter();
+ },
+ _popThreadPause: function TA__popThreadPause() {
+ const eventLoop = this._threadPauseEventLoops.pop();
+ dbg_assert(eventLoop, "Should have an event loop.");
+ eventLoop.resolve();
+ },
+
clearDebuggees: function TA_clearDebuggees() {
if (this.dbg) {
this.dbg.removeAllDebuggees();
@@ -485,7 +640,7 @@ ThreadActor.prototype = {
this.conn.send(packet);
// Start a nested event loop.
- this._nest();
+ this._pushThreadPause();
// We already sent a response to this request, don't send one
// now.
@@ -529,7 +684,7 @@ ThreadActor.prototype = {
* promise.
*/
_pauseAndRespond: function TA__pauseAndRespond(aFrame, aReason,
- onPacket=function (k) k) {
+ onPacket=function (k) { return k; }) {
try {
let packet = this._paused(aFrame);
if (!packet) {
@@ -548,14 +703,17 @@ ThreadActor.prototype = {
message: error.message + "\n" + error.stack
};
})
- .then(packet => this.conn.send(packet));
+ .then(packet => {
+ this.conn.send(packet)
+ });
});
- return this._nest();
+ this._pushThreadPause();
} catch(e) {
reportError(e, "Got an exception during TA__pauseAndRespond: ");
- return undefined;
}
+
+ return undefined;
},
/**
@@ -573,13 +731,13 @@ ThreadActor.prototype = {
// In case of multiple nested event loops (due to multiple debuggers open in
// different tabs or multiple debugger clients connected to the same tab)
// only allow resumption in a LIFO order.
- if (DebuggerServer.xpcInspector.eventLoopNestLevel > 1) {
- let lastNestRequestor = DebuggerServer.xpcInspector.lastNestRequestor;
- if (lastNestRequestor.connection != this.conn) {
- return { error: "wrongOrder",
- message: "trying to resume in the wrong order.",
- lastPausedUrl: lastNestRequestor.url };
- }
+ if (this._nestedEventLoops.size
+ && this._nestedEventLoops.lastPausedUrl !== this._hooks.url) {
+ return {
+ error: "wrongOrder",
+ message: "trying to resume in the wrong order.",
+ lastPausedUrl: this._nestedEventLoops.lastPausedUrl
+ };
}
if (aRequest && aRequest.forceCompletion) {
@@ -592,7 +750,7 @@ ThreadActor.prototype = {
this.dbg.getNewestFrame().pop(aRequest.completionValue);
let packet = this._resumed();
- DebuggerServer.xpcInspector.exitNestedEventLoop();
+ this._popThreadPause();
return { type: "resumeLimit", frameFinished: aRequest.forceCompletion };
}
@@ -614,17 +772,27 @@ ThreadActor.prototype = {
// Define the JS hook functions for stepping.
let onEnterFrame = aFrame => {
- if (this.sources.isBlackBoxed(aFrame.script.url)) {
- return undefined;
- }
- return pauseAndRespond(aFrame);
+ let { url } = this.synchronize(this.sources.getOriginalLocation(
+ aFrame.script.url,
+ aFrame.script.getOffsetLine(aFrame.offset),
+ getOffsetColumn(aFrame.offset, aFrame.script)));
+
+ return this.sources.isBlackBoxed(url)
+ ? undefined
+ : pauseAndRespond(aFrame);
};
let thread = this;
let onPop = function TA_onPop(aCompletion) {
// onPop is called with 'this' set to the current frame.
- if (thread.sources.isBlackBoxed(this.script.url)) {
+
+ let { url } = thread.synchronize(thread.sources.getOriginalLocation(
+ this.script.url,
+ this.script.getOffsetLine(this.offset),
+ getOffsetColumn(this.offset, this.script)));
+
+ if (thread.sources.isBlackBoxed(url)) {
return undefined;
}
@@ -650,7 +818,12 @@ ThreadActor.prototype = {
let onStep = function TA_onStep() {
// onStep is called with 'this' set to the current frame.
- if (thread.sources.isBlackBoxed(this.script.url)) {
+ let { url } = thread.synchronize(thread.sources.getOriginalLocation(
+ this.script.url,
+ this.script.getOffsetLine(this.offset),
+ getOffsetColumn(this.offset, this.script)));
+
+ if (thread.sources.isBlackBoxed(url)) {
return undefined;
}
@@ -712,10 +885,44 @@ ThreadActor.prototype = {
}
let packet = this._resumed();
- DebuggerServer.xpcInspector.exitNestedEventLoop();
+ this._popThreadPause();
return packet;
},
+ /**
+ * Spin up a nested event loop so we can synchronously resolve a promise.
+ *
+ * @param aPromise
+ * The promise we want to resolve.
+ * @returns The promise's resolution.
+ */
+ synchronize: function(aPromise) {
+ let needNest = true;
+ let eventLoop;
+ let returnVal;
+
+ aPromise
+ .then((aResolvedVal) => {
+ needNest = false;
+ returnVal = aResolvedVal;
+ })
+ .then(null, (aError) => {
+ reportError(aError, "Error inside synchronize:");
+ })
+ .then(() => {
+ if (eventLoop) {
+ eventLoop.resolve();
+ }
+ });
+
+ if (needNest) {
+ eventLoop = this._nestedEventLoops.push();
+ eventLoop.enter();
+ }
+
+ return returnVal;
+ },
+
/**
* Set the debugging hook to pause on exceptions if configured to do so.
*/
@@ -1278,7 +1485,7 @@ ThreadActor.prototype = {
this.conn.send(packet);
// Start a nested event loop.
- this._nest();
+ this._pushThreadPause();
// We already sent a response to this request, don't send one
// now.
@@ -1428,26 +1635,6 @@ ThreadActor.prototype = {
return packet;
},
- _nest: function TA_nest() {
- if (this._hooks.preNest) {
- var nestData = this._hooks.preNest();
- }
-
- let requestor = Object.create(null);
- requestor.url = this._hooks.url;
- requestor.connection = this.conn;
- DebuggerServer.xpcInspector.enterNestedEventLoop(requestor);
-
- dbg_assert(this.state === "running", "Should be in the running state");
-
- if (this._hooks.postNest) {
- this._hooks.postNest(nestData)
- }
-
- // "continue" resumption value.
- return undefined;
- },
-
_resumed: function TA_resumed() {
this._state = "running";
@@ -1753,10 +1940,14 @@ ThreadActor.prototype = {
onDebuggerStatement: function TA_onDebuggerStatement(aFrame) {
// Don't pause if we are currently stepping (in or over) or the frame is
// black-boxed.
- if (this.sources.isBlackBoxed(aFrame.script.url) || aFrame.onStep) {
- return undefined;
- }
- return this._pauseAndRespond(aFrame, { type: "debuggerStatement" });
+ let { url } = this.synchronize(this.sources.getOriginalLocation(
+ aFrame.script.url,
+ aFrame.script.getOffsetLine(aFrame.offset),
+ getOffsetColumn(aFrame.offset, aFrame.script)));
+
+ return this.sources.isBlackBoxed(url) || aFrame.onStep
+ ? undefined
+ : this._pauseAndRespond(aFrame, { type: "debuggerStatement" });
},
/**
@@ -1769,9 +1960,15 @@ ThreadActor.prototype = {
* The exception that was thrown.
*/
onExceptionUnwind: function TA_onExceptionUnwind(aFrame, aValue) {
- if (this.sources.isBlackBoxed(aFrame.script.url)) {
+ let { url } = this.synchronize(this.sources.getOriginalLocation(
+ aFrame.script.url,
+ aFrame.script.getOffsetLine(aFrame.offset),
+ getOffsetColumn(aFrame.offset, aFrame.script)));
+
+ if (this.sources.isBlackBoxed(url)) {
return undefined;
}
+
try {
let packet = this._paused(aFrame);
if (!packet) {
@@ -1781,11 +1978,13 @@ ThreadActor.prototype = {
packet.why = { type: "exception",
exception: this.createValueGrip(aValue) };
this.conn.send(packet);
- return this._nest();
+
+ this._pushThreadPause();
} catch(e) {
reportError(e, "Got an exception during TA_onExceptionUnwind: ");
- return undefined;
}
+
+ return undefined;
},
/**
@@ -2781,8 +2980,13 @@ BreakpointActor.prototype = {
hit: function BA_hit(aFrame) {
// Don't pause if we are currently stepping (in or over) or the frame is
// black-boxed.
- if (this.threadActor.sources.isBlackBoxed(this.location.url) ||
- aFrame.onStep) {
+ let { url } = this.threadActor.synchronize(
+ this.threadActor.sources.getOriginalLocation(
+ this.location.url,
+ this.location.line,
+ this.location.column));
+
+ if (this.threadActor.sources.isBlackBoxed(url) || aFrame.onStep) {
return undefined;
}
diff --git a/toolkit/devtools/server/tests/unit/head_dbg.js b/toolkit/devtools/server/tests/unit/head_dbg.js
index 746a14f9ecc5..4d95bb5576d3 100644
--- a/toolkit/devtools/server/tests/unit/head_dbg.js
+++ b/toolkit/devtools/server/tests/unit/head_dbg.js
@@ -18,6 +18,7 @@ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
+Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
function testExceptionHook(ex) {
try {
@@ -346,3 +347,9 @@ function StubTransport() { }
StubTransport.prototype.ready = function () {};
StubTransport.prototype.send = function () {};
StubTransport.prototype.close = function () {};
+
+function executeSoon(aFunc) {
+ Services.tm.mainThread.dispatch({
+ run: DevToolsUtils.makeInfallible(aFunc)
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+}
diff --git a/toolkit/devtools/server/tests/unit/test_blackboxing-06.js b/toolkit/devtools/server/tests/unit/test_blackboxing-06.js
new file mode 100644
index 000000000000..009ec285e1e7
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_blackboxing-06.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we can black box source mapped sources.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+
+const promise = devtools.require("sdk/core/promise");
+
+function run_test()
+{
+ initTestDebuggerServer();
+ gDebuggee = addTestGlobal("test-black-box");
+ gClient = new DebuggerClient(DebuggerServer.connectPipe());
+ gClient.connect(function() {
+ attachTestTabAndResume(gClient, "test-black-box", function(aResponse, aTabClient, aThreadClient) {
+ gThreadClient = aThreadClient;
+
+ promise.resolve(setup_code())
+ .then(black_box_code)
+ .then(run_code)
+ .then(test_correct_location)
+ .then(null, function (error) {
+ do_check_true(false, "Should not get an error, got " + error);
+ })
+ .then(function () {
+ finishClient(gClient);
+ });
+ });
+ });
+ do_test_pending();
+}
+
+function setup_code() {
+ let { code, map } = (new SourceNode(null, null, null, [
+ new SourceNode(1, 0, "a.js", "" + function a() {
+ return b();
+ }),
+ "\n",
+ new SourceNode(1, 0, "b.js", "" + function b() {
+ debugger; // Don't want to stop here.
+ return c();
+ }),
+ "\n",
+ new SourceNode(1, 0, "c.js", "" + function c() {
+ debugger; // Want to stop here.
+ }),
+ "\n"
+ ])).toStringWithSourceMap({
+ file: "abc.js",
+ sourceRoot: "http://example.com/"
+ });
+
+ code += "//# sourceMappingURL=data:text/json," + map.toString();
+
+ Components.utils.evalInSandbox(code,
+ gDebuggee,
+ "1.8",
+ "http://example.com/abc.js");
+}
+
+function black_box_code() {
+ const d = promise.defer();
+
+ gThreadClient.getSources(function ({ sources, error }) {
+ do_check_true(!error, "Shouldn't get an error getting sources");
+ const source = sources.filter((s) => {
+ return s.url.indexOf("b.js") !== -1;
+ })[0];
+ do_check_true(!!source, "We should have our source in the sources list");
+
+ gThreadClient.source(source).blackBox(function ({ error }) {
+ do_check_true(!error, "Should not get an error black boxing");
+ d.resolve(true);
+ });
+ });
+
+ return d.promise;
+}
+
+function run_code() {
+ const d = promise.defer();
+
+ gClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+ d.resolve(aPacket);
+ gThreadClient.resume();
+ });
+ gDebuggee.a();
+
+ return d.promise;
+}
+
+function test_correct_location(aPacket) {
+ do_check_eq(aPacket.why.type, "debuggerStatement",
+ "Should hit a debugger statement.");
+ do_check_eq(aPacket.frame.where.url, "http://example.com/c.js",
+ "Should have skipped over the debugger statement in the black boxed source");
+}
diff --git a/toolkit/devtools/server/tests/unit/test_nesting-01.js b/toolkit/devtools/server/tests/unit/test_nesting-01.js
new file mode 100644
index 000000000000..bae0edf015d0
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_nesting-01.js
@@ -0,0 +1,38 @@
+/* -*- Mode: javascript; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can nest event loops when needed in
+// ThreadActor.prototype.synchronize.
+
+const { defer } = devtools.require("sdk/core/promise");
+
+function run_test() {
+ initTestDebuggerServer();
+ do_test_pending();
+ test_nesting();
+}
+
+function test_nesting() {
+ const thread = new DebuggerServer.ThreadActor(DebuggerServer);
+ const { resolve, reject, promise } = defer();
+
+ let currentStep = 0;
+
+ executeSoon(function () {
+ // Should be on the first step
+ do_check_eq(++currentStep, 1);
+ // We should have one nested event loop from synchronize
+ do_check_eq(thread._nestedEventLoops.size, 1);
+ resolve(true);
+ });
+
+ do_check_eq(thread.synchronize(promise), true);
+
+ // Should be on the second step
+ do_check_eq(++currentStep, 2);
+ // There shouldn't be any nested event loops anymore
+ do_check_eq(thread._nestedEventLoops.size, 0);
+
+ do_test_finished();
+}
diff --git a/toolkit/devtools/server/tests/unit/test_nesting-02.js b/toolkit/devtools/server/tests/unit/test_nesting-02.js
new file mode 100644
index 000000000000..ce1f9aa64daf
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_nesting-02.js
@@ -0,0 +1,68 @@
+/* -*- Mode: javascript; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can nest event loops and then automatically exit nested event
+// loops when requested.
+
+const { defer } = devtools.require("sdk/core/promise");
+
+function run_test() {
+ initTestDebuggerServer();
+ do_test_pending();
+ test_nesting();
+}
+
+function test_nesting() {
+ const thread = new DebuggerServer.ThreadActor(DebuggerServer);
+ const { resolve, reject, promise } = defer();
+
+ // The following things should happen (in order):
+ // 1. In the new event loop (created by synchronize)
+ // 2. Resolve the promise (shouldn't exit any event loops)
+ // 3. Exit the event loop (should also then exit synchronize's event loop)
+ // 4. Be after the synchronize call
+ let currentStep = 0;
+
+ executeSoon(function () {
+ let eventLoop;
+
+ executeSoon(function () {
+ // Should be at step 2
+ do_check_eq(++currentStep, 2);
+ // Before resolving, should have the synchronize event loop and the one just created.
+ do_check_eq(thread._nestedEventLoops.size, 2);
+
+ executeSoon(function () {
+ // Should be at step 3
+ do_check_eq(++currentStep, 3);
+ // Before exiting the manually created event loop, should have the
+ // synchronize event loop and the manual event loop.
+ do_check_eq(thread._nestedEventLoops.size, 2);
+ // Should have the event loop
+ do_check_true(!!eventLoop);
+ eventLoop.resolve();
+ });
+
+ resolve(true);
+ // Shouldn't exit any event loops because a new one started since the call to synchronize
+ do_check_eq(thread._nestedEventLoops.size, 2);
+ });
+
+ // Should be at step 1
+ do_check_eq(++currentStep, 1);
+ // Should have only the synchronize event loop
+ do_check_eq(thread._nestedEventLoops.size, 1);
+ eventLoop = thread._nestedEventLoops.push();
+ eventLoop.enter();
+ });
+
+ do_check_eq(thread.synchronize(promise), true);
+
+ // Should be on the fourth step
+ do_check_eq(++currentStep, 4);
+ // There shouldn't be any nested event loops anymore
+ do_check_eq(thread._nestedEventLoops.size, 0);
+
+ do_test_finished();
+}
diff --git a/toolkit/devtools/server/tests/unit/test_sourcemaps-02.js b/toolkit/devtools/server/tests/unit/test_sourcemaps-02.js
index 76e5a170ee68..6d63c9a2f261 100644
--- a/toolkit/devtools/server/tests/unit/test_sourcemaps-02.js
+++ b/toolkit/devtools/server/tests/unit/test_sourcemaps-02.js
@@ -33,8 +33,6 @@ function test_simple_source_map()
"http://example.com/www/js/b.js",
"http://example.com/www/js/c.js"]);
- let numNewSources = 0;
-
gClient.addOneTimeListener("paused", function (aEvent, aPacket) {
gThreadClient.getSources(function (aResponse) {
do_check_true(!aResponse.error, "Should not get an error");
diff --git a/toolkit/devtools/server/tests/unit/xpcshell.ini b/toolkit/devtools/server/tests/unit/xpcshell.ini
index 1d997db9ad8e..9c96bb55b1ec 100644
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -2,6 +2,8 @@
head = head_dbg.js
tail =
+[test_nesting-01.js]
+[test_nesting-02.js]
[test_forwardingprefix.js]
[test_getyoungestframe.js]
[test_nsjsinspector.js]
@@ -18,6 +20,7 @@ reason = bug 821285
[test_blackboxing-03.js]
[test_blackboxing-04.js]
[test_blackboxing-05.js]
+[test_blackboxing-06.js]
[test_frameactor-01.js]
[test_frameactor-02.js]
[test_frameactor-03.js]
From d9cd7910a4c23e2ad3c7598eab98c11f10d822ba Mon Sep 17 00:00:00 2001
From: Nick Fitzgerald
Date: Fri, 16 Aug 2013 15:02:39 -0700
Subject: [PATCH 28/46] Bug 905046 - black boxing eye not shown; r=vporof
---
browser/themes/linux/devtools/blackbox.png | Bin 1005 -> 0 bytes
browser/themes/linux/devtools/debugger.css | 3 ++-
browser/themes/linux/jar.mn | 1 -
browser/themes/osx/devtools/blackbox.png | Bin 1005 -> 0 bytes
browser/themes/osx/devtools/debugger.css | 3 ++-
browser/themes/osx/jar.mn | 1 -
browser/themes/windows/devtools/blackbox.png | Bin 1005 -> 0 bytes
browser/themes/windows/devtools/debugger.css | 3 ++-
browser/themes/windows/jar.mn | 2 --
9 files changed, 6 insertions(+), 7 deletions(-)
delete mode 100644 browser/themes/linux/devtools/blackbox.png
delete mode 100644 browser/themes/osx/devtools/blackbox.png
delete mode 100644 browser/themes/windows/devtools/blackbox.png
diff --git a/browser/themes/linux/devtools/blackbox.png b/browser/themes/linux/devtools/blackbox.png
deleted file mode 100644
index 11a45b5d037853f7cfae2d266269382acb0cd558..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1005
zcmVlte0$FKJ9|9yof%eDRpwuQSiRBmZx`SV%>FT6W?A*u
z3*dg#>Ggfq+Z&b@Z%=RE`Mh;SjnpVcpTu!|L+R#C*W21Vla~G`
z_qy77_+oeqaupam>;N^2sSn-)U(|g0!ou$|KJ37urc;9JtM`V>6L{?O`1?A#A09?=
zu57*2RZ5Lw>Z38YC-B7Kp1z1aJryuDcpZEZd?j*h$jw22GOS3AV(OzY_)7To=!+i!
zn#kdrKI@(d^g6yu_c2M2wR~HCTW9x%8UO4{Y801LRTt10<$Uo5`X(H%qp&)f2)+Q`
zhOsi_RKTag7-6fZQ4F6(W0V7LgU=;PMaYFhvs@Z)2qIR{RFh~7DY7g?qA@S+5@NQD1nH2hK>1rK4XZ&}EM@H+i
z*Ay?emTfgtOno#)IW@KWOUP)J?j;W|VhnZQFF|f8OcuNWUZ9xzXpC~;Gsuu-rC^vb
zW#!RUW~?j7&u+eQY5Ur>`BuTei)?($cZ;1|nRConG0PlpK)vU@&q4^`tm@@NtTvE|QOID`w*pY)P@^W+NVZwf#
zoD8oNfiN#A-jDcwj8Xzx|$BvVg_c|L1M^
bO@ILaIdut6%kstp00000NkvXXu0mjfrUB=;
diff --git a/browser/themes/linux/devtools/debugger.css b/browser/themes/linux/devtools/debugger.css
index 297217f7667e..b6c0e727aef2 100644
--- a/browser/themes/linux/devtools/debugger.css
+++ b/browser/themes/linux/devtools/debugger.css
@@ -40,9 +40,10 @@
.side-menu-widget-item-checkbox > .checkbox-check {
-moz-appearance: none;
background: none;
- background-image: url(blackbox.png);
+ background-image: url(itemToggle.png);
background-repeat: no-repeat;
background-clip: content-box;
+ background-size: 32px 16px;
background-position: -16px 0;
width: 16px;
height: 16px;
diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn
index 49b21d75844a..527636f29e38 100644
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -179,7 +179,6 @@ browser.jar:
skin/classic/browser/devtools/magnifying-glass.png (devtools/magnifying-glass.png)
skin/classic/browser/devtools/option-icon.png (devtools/option-icon.png)
skin/classic/browser/devtools/itemToggle.png (devtools/itemToggle.png)
- skin/classic/browser/devtools/blackbox.png (devtools/blackbox.png)
skin/classic/browser/devtools/blackBoxMessageEye.png (devtools/blackBoxMessageEye.png)
skin/classic/browser/devtools/itemArrow-rtl.png (devtools/itemArrow-rtl.png)
skin/classic/browser/devtools/itemArrow-ltr.png (devtools/itemArrow-ltr.png)
diff --git a/browser/themes/osx/devtools/blackbox.png b/browser/themes/osx/devtools/blackbox.png
deleted file mode 100644
index 11a45b5d037853f7cfae2d266269382acb0cd558..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1005
zcmVlte0$FKJ9|9yof%eDRpwuQSiRBmZx`SV%>FT6W?A*u
z3*dg#>Ggfq+Z&b@Z%=RE`Mh;SjnpVcpTu!|L+R#C*W21Vla~G`
z_qy77_+oeqaupam>;N^2sSn-)U(|g0!ou$|KJ37urc;9JtM`V>6L{?O`1?A#A09?=
zu57*2RZ5Lw>Z38YC-B7Kp1z1aJryuDcpZEZd?j*h$jw22GOS3AV(OzY_)7To=!+i!
zn#kdrKI@(d^g6yu_c2M2wR~HCTW9x%8UO4{Y801LRTt10<$Uo5`X(H%qp&)f2)+Q`
zhOsi_RKTag7-6fZQ4F6(W0V7LgU=;PMaYFhvs@Z)2qIR{RFh~7DY7g?qA@S+5@NQD1nH2hK>1rK4XZ&}EM@H+i
z*Ay?emTfgtOno#)IW@KWOUP)J?j;W|VhnZQFF|f8OcuNWUZ9xzXpC~;Gsuu-rC^vb
zW#!RUW~?j7&u+eQY5Ur>`BuTei)?($cZ;1|nRConG0PlpK)vU@&q4^`tm@@NtTvE|QOID`w*pY)P@^W+NVZwf#
zoD8oNfiN#A-jDcwj8Xzx|$BvVg_c|L1M^
bO@ILaIdut6%kstp00000NkvXXu0mjfrUB=;
diff --git a/browser/themes/osx/devtools/debugger.css b/browser/themes/osx/devtools/debugger.css
index a65c4750ba02..07cc9133796f 100644
--- a/browser/themes/osx/devtools/debugger.css
+++ b/browser/themes/osx/devtools/debugger.css
@@ -42,9 +42,10 @@
.side-menu-widget-item-checkbox > .checkbox-check {
-moz-appearance: none;
background: none;
- background-image: url(blackbox.png);
+ background-image: url(itemToggle.png);
background-repeat: no-repeat;
background-clip: content-box;
+ background-size: 32px 16px;
background-position: -16px 0;
width: 16px;
height: 16px;
diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn
index 6860bc4cd74c..22d99b2e3e52 100644
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -270,7 +270,6 @@ browser.jar:
skin/classic/browser/devtools/magnifying-glass.png (devtools/magnifying-glass.png)
skin/classic/browser/devtools/option-icon.png (devtools/option-icon.png)
skin/classic/browser/devtools/itemToggle.png (devtools/itemToggle.png)
- skin/classic/browser/devtools/blackbox.png (devtools/blackbox.png)
skin/classic/browser/devtools/blackBoxMessageEye.png (devtools/blackBoxMessageEye.png)
skin/classic/browser/devtools/itemArrow-rtl.png (devtools/itemArrow-rtl.png)
skin/classic/browser/devtools/itemArrow-ltr.png (devtools/itemArrow-ltr.png)
diff --git a/browser/themes/windows/devtools/blackbox.png b/browser/themes/windows/devtools/blackbox.png
deleted file mode 100644
index 11a45b5d037853f7cfae2d266269382acb0cd558..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1005
zcmVlte0$FKJ9|9yof%eDRpwuQSiRBmZx`SV%>FT6W?A*u
z3*dg#>Ggfq+Z&b@Z%=RE`Mh;SjnpVcpTu!|L+R#C*W21Vla~G`
z_qy77_+oeqaupam>;N^2sSn-)U(|g0!ou$|KJ37urc;9JtM`V>6L{?O`1?A#A09?=
zu57*2RZ5Lw>Z38YC-B7Kp1z1aJryuDcpZEZd?j*h$jw22GOS3AV(OzY_)7To=!+i!
zn#kdrKI@(d^g6yu_c2M2wR~HCTW9x%8UO4{Y801LRTt10<$Uo5`X(H%qp&)f2)+Q`
zhOsi_RKTag7-6fZQ4F6(W0V7LgU=;PMaYFhvs@Z)2qIR{RFh~7DY7g?qA@S+5@NQD1nH2hK>1rK4XZ&}EM@H+i
z*Ay?emTfgtOno#)IW@KWOUP)J?j;W|VhnZQFF|f8OcuNWUZ9xzXpC~;Gsuu-rC^vb
zW#!RUW~?j7&u+eQY5Ur>`BuTei)?($cZ;1|nRConG0PlpK)vU@&q4^`tm@@NtTvE|QOID`w*pY)P@^W+NVZwf#
zoD8oNfiN#A-jDcwj8Xzx|$BvVg_c|L1M^
bO@ILaIdut6%kstp00000NkvXXu0mjfrUB=;
diff --git a/browser/themes/windows/devtools/debugger.css b/browser/themes/windows/devtools/debugger.css
index 8366da1022f1..418a44d3f122 100644
--- a/browser/themes/windows/devtools/debugger.css
+++ b/browser/themes/windows/devtools/debugger.css
@@ -40,9 +40,10 @@
.side-menu-widget-item-checkbox > .checkbox-check {
-moz-appearance: none;
background: none;
- background-image: url(blackbox.png);
+ background-image: url(itemToggle.png);
background-repeat: no-repeat;
background-clip: content-box;
+ background-size: 32px 16px;
background-position: -16px 0;
width: 16px;
height: 16px;
diff --git a/browser/themes/windows/jar.mn b/browser/themes/windows/jar.mn
index b9d836e6d6af..c4fae06db831 100644
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -206,7 +206,6 @@ browser.jar:
skin/classic/browser/devtools/magnifying-glass.png (devtools/magnifying-glass.png)
skin/classic/browser/devtools/option-icon.png (devtools/option-icon.png)
skin/classic/browser/devtools/itemToggle.png (devtools/itemToggle.png)
- skin/classic/browser/devtools/blackbox.png (devtools/blackbox.png)
skin/classic/browser/devtools/blackBoxMessageEye.png (devtools/blackBoxMessageEye.png)
skin/classic/browser/devtools/itemArrow-rtl.png (devtools/itemArrow-rtl.png)
skin/classic/browser/devtools/itemArrow-ltr.png (devtools/itemArrow-ltr.png)
@@ -465,7 +464,6 @@ browser.jar:
skin/classic/aero/browser/devtools/magnifying-glass.png (devtools/magnifying-glass.png)
skin/classic/aero/browser/devtools/option-icon.png (devtools/option-icon.png)
skin/classic/aero/browser/devtools/itemToggle.png (devtools/itemToggle.png)
- skin/classic/browser/devtools/blackbox.png (devtools/blackbox.png)
skin/classic/aero/browser/devtools/blackBoxMessageEye.png (devtools/blackBoxMessageEye.png)
skin/classic/aero/browser/devtools/itemArrow-rtl.png (devtools/itemArrow-rtl.png)
skin/classic/aero/browser/devtools/background-noise-toolbar.png (devtools/background-noise-toolbar.png)
From 04207e6faf8fe9e23a2a2d28e7777bf07c2d835d Mon Sep 17 00:00:00 2001
From: Ryan VanderMeulen
Date: Fri, 16 Aug 2013 18:21:53 -0400
Subject: [PATCH 29/46] Backed out changeset b0bab66c0cab (bug 896927) for
Windows xpcshell failures.
---
.../downloads/src/DownloadsCommon.jsm | 149 +++++++++++++-----
.../jsdownloads/src/DownloadIntegration.jsm | 27 +---
.../jsdownloads/src/DownloadUIHelper.jsm | 144 -----------------
3 files changed, 107 insertions(+), 213 deletions(-)
diff --git a/browser/components/downloads/src/DownloadsCommon.jsm b/browser/components/downloads/src/DownloadsCommon.jsm
index 98f4d1fa7d10..e9722019a9ae 100644
--- a/browser/components/downloads/src/DownloadsCommon.jsm
+++ b/browser/components/downloads/src/DownloadsCommon.jsm
@@ -53,8 +53,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
- "resource://gre/modules/DownloadUIHelper.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
"resource://gre/modules/DownloadUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
@@ -70,9 +68,27 @@ XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
const nsIDM = Ci.nsIDownloadManager;
+const kDownloadsStringBundleUrl =
+ "chrome://browser/locale/downloads/downloads.properties";
+
const kPrefBdmScanWhenDone = "browser.download.manager.scanWhenDone";
const kPrefBdmAlertOnExeOpen = "browser.download.manager.alertOnEXEOpen";
+const kDownloadsStringsRequiringFormatting = {
+ sizeWithUnits: true,
+ shortTimeLeftSeconds: true,
+ shortTimeLeftMinutes: true,
+ shortTimeLeftHours: true,
+ shortTimeLeftDays: true,
+ statusSeparator: true,
+ statusSeparatorBeforeNumber: true,
+ fileExecutableSecurityWarning: true
+};
+
+const kDownloadsStringsRequiringPluralForm = {
+ otherDownloads2: true
+};
+
XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
return Components.Constructor("@mozilla.org/file/local;1",
"nsILocalFile", "initWithPath");
@@ -147,6 +163,41 @@ this.DownloadsCommon = {
}
this.error.apply(this, aMessageArgs);
},
+ /**
+ * Returns an object whose keys are the string names from the downloads string
+ * bundle, and whose values are either the translated strings or functions
+ * returning formatted strings.
+ */
+ get strings()
+ {
+ let strings = {};
+ let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
+ let enumerator = sb.getSimpleEnumeration();
+ while (enumerator.hasMoreElements()) {
+ let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
+ let stringName = string.key;
+ if (stringName in kDownloadsStringsRequiringFormatting) {
+ strings[stringName] = function () {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ return sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ };
+ } else if (stringName in kDownloadsStringsRequiringPluralForm) {
+ strings[stringName] = function (aCount) {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ let formattedString = sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ return PluralForm.get(aCount, formattedString);
+ };
+ } else {
+ strings[stringName] = string.value;
+ }
+ }
+ delete this.strings;
+ return this.strings = strings;
+ },
/**
* Generates a very short string representing the given time left.
@@ -429,44 +480,65 @@ this.DownloadsCommon = {
if (!(aOwnerWindow instanceof Ci.nsIDOMWindow))
throw new Error("aOwnerWindow must be a dom-window object");
- let promiseShouldLaunch;
+ // Confirm opening executable files if required.
if (aFile.isExecutable()) {
- // We get a prompter for the provided window here, even though anchoring
- // to the most recently active window should work as well.
- promiseShouldLaunch =
- DownloadUIHelper.getPrompter(aOwnerWindow)
- .confirmLaunchExecutable(aFile.path);
- } else {
- promiseShouldLaunch = Promise.resolve(true);
- }
-
- promiseShouldLaunch.then(shouldLaunch => {
- if (!shouldLaunch) {
- return;
- }
-
- // Actually open the file.
+ let showAlert = true;
try {
- if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
- aMimeInfo.launchWithFile(aFile);
+ showAlert = Services.prefs.getBoolPref(kPrefBdmAlertOnExeOpen);
+ } catch (ex) { }
+
+ // On Vista and above, we rely on native security prompting for
+ // downloaded content unless it's disabled.
+ if (DownloadsCommon.isWinVistaOrHigher) {
+ try {
+ if (Services.prefs.getBoolPref(kPrefBdmScanWhenDone)) {
+ showAlert = false;
+ }
+ } catch (ex) { }
+ }
+
+ if (showAlert) {
+ let name = aFile.leafName;
+ let message =
+ DownloadsCommon.strings.fileExecutableSecurityWarning(name, name);
+ let title =
+ DownloadsCommon.strings.fileExecutableSecurityWarningTitle;
+ let dontAsk =
+ DownloadsCommon.strings.fileExecutableSecurityWarningDontAsk;
+
+ let checkbox = { value: false };
+ let open = Services.prompt.confirmCheck(aOwnerWindow, title, message,
+ dontAsk, checkbox);
+ if (!open) {
return;
}
+
+ Services.prefs.setBoolPref(kPrefBdmAlertOnExeOpen,
+ !checkbox.value);
}
- catch(ex) { }
-
- // If either we don't have the mime info, or the preferred action failed,
- // attempt to launch the file directly.
- try {
- aFile.launch();
+ }
+
+ // Actually open the file.
+ try {
+ if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
+ aMimeInfo.launchWithFile(aFile);
+ return;
}
- catch(ex) {
- // If launch fails, try sending it through the system's external "file:"
- // URL handler.
- Cc["@mozilla.org/uriloader/external-protocol-service;1"]
- .getService(Ci.nsIExternalProtocolService)
- .loadUrl(NetUtil.newURI(aFile));
- }
- }).then(null, Cu.reportError);
+ }
+ catch(ex) { }
+
+ // If either we don't have the mime info, or the preferred action failed,
+ // attempt to launch the file directly.
+ try {
+ aFile.launch();
+ }
+ catch(ex) {
+ // If launch fails, try sending it through the system's external "file:"
+ // URL handler.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadUrl(NetUtil.newURI(aFile));
+ }
},
/**
@@ -502,15 +574,6 @@ this.DownloadsCommon = {
}
};
-/**
- * Returns an object whose keys are the string names from the downloads string
- * bundle, and whose values are either the translated strings or functions
- * returning formatted strings.
- */
-XPCOMUtils.defineLazyGetter(DownloadsCommon, "strings", function () {
- return DownloadUIHelper.strings;
-});
-
/**
* Returns true if we are executing on Windows Vista or a later version.
*/
diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
index 9885753883a8..abf660de8c3e 100644
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -29,8 +29,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
"resource://gre/modules/DownloadStore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
"resource://gre/modules/DownloadImport.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
- "resource://gre/modules/DownloadUIHelper.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
@@ -63,7 +61,6 @@ XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
return null;
});
-// This will be replaced by "DownloadUIHelper.strings" (see bug 905123).
XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
return Services.strings.
createBundle("chrome://mozapps/locale/downloads/downloads.properties");
@@ -417,24 +414,6 @@ this.DownloadIntegration = {
let deferred = Task.spawn(function DI_launchDownload_task() {
let file = new FileUtils.File(aDownload.target.path);
- // Ask for confirmation if the file is executable. We do this here,
- // instead of letting the caller handle the prompt separately in the user
- // interface layer, for two reasons. The first is because of its security
- // nature, so that add-ons cannot forget to do this check. The second is
- // that the system-level security prompt, if enabled, would be displayed
- // at launch time in any case.
- if (file.isExecutable() && !this.dontOpenFileAndFolder) {
- // We don't anchor the prompt to a specific window intentionally, not
- // only because this is the same behavior as the system-level prompt,
- // but also because the most recently active window is the right choice
- // in basically all cases.
- let shouldLaunch = yield DownloadUIHelper.getPrompter()
- .confirmLaunchExecutable(file.path);
- if (!shouldLaunch) {
- return;
- }
- }
-
// In case of a double extension, like ".tar.gz", we only
// consider the last one, because the MIME service cannot
// handle multiple extensions.
@@ -581,11 +560,7 @@ this.DownloadIntegration = {
*/
_createDownloadsDirectory: function DI_createDownloadsDirectory(aName) {
let directory = this._getDirectory(aName);
-
- // We read the name of the directory from the list of translated strings
- // that is kept by the UI helper module, even if this string is not strictly
- // displayed in the user interface.
- directory.append(DownloadUIHelper.strings.downloadsFolder);
+ directory.append(gStringBundle.GetStringFromName("downloadsFolder"));
// Create the Downloads folder and ignore if it already exists.
return OS.File.makeDir(directory.path, { ignoreExisting: true }).
diff --git a/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
index 8013c9d476e7..297abe0dcfeb 100644
--- a/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
@@ -24,31 +24,6 @@ const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OS",
- "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
- "resource://gre/modules/commonjs/sdk/core/promise.js");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
- "resource://gre/modules/Services.jsm");
-
-const kStringBundleUrl =
- "chrome://browser/locale/downloads/downloads.properties";
-
-const kStringsRequiringFormatting = {
- sizeWithUnits: true,
- shortTimeLeftSeconds: true,
- shortTimeLeftMinutes: true,
- shortTimeLeftHours: true,
- shortTimeLeftDays: true,
- statusSeparator: true,
- statusSeparatorBeforeNumber: true,
- fileExecutableSecurityWarning: true,
-};
-
-const kStringsRequiringPluralForm = {
- otherDownloads2: true,
-};
-
////////////////////////////////////////////////////////////////////////////////
//// DownloadUIHelper
@@ -56,123 +31,4 @@ const kStringsRequiringPluralForm = {
* Provides functions to handle status and messages in the user interface.
*/
this.DownloadUIHelper = {
- /**
- * Returns an object that can be used to display prompts related to downloads.
- *
- * The prompts may be either anchored to a specified window, or anchored to
- * the most recently active window, for example if the prompt is displayed in
- * response to global notifications that are not associated with any window.
- *
- * @param aParent
- * If specified, should reference the nsIDOMWindow to which the prompts
- * should be attached. If omitted, the prompts will be attached to the
- * most recently active window.
- *
- * @return A DownloadPrompter object.
- */
- getPrompter: function (aParent)
- {
- return new DownloadPrompter(aParent || null);
- },
-};
-
-/**
- * Returns an object whose keys are the string names from the downloads string
- * bundle, and whose values are either the translated strings or functions
- * returning formatted strings.
- */
-XPCOMUtils.defineLazyGetter(DownloadUIHelper, "strings", function () {
- let strings = {};
- let sb = Services.strings.createBundle(kStringBundleUrl);
- let enumerator = sb.getSimpleEnumeration();
- while (enumerator.hasMoreElements()) {
- let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
- let stringName = string.key;
- if (stringName in kStringsRequiringFormatting) {
- strings[stringName] = function () {
- // Convert "arguments" to a real array before calling into XPCOM.
- return sb.formatStringFromName(stringName,
- Array.slice(arguments, 0),
- arguments.length);
- };
- } else if (stringName in kStringsRequiringPluralForm) {
- strings[stringName] = function (aCount) {
- // Convert "arguments" to a real array before calling into XPCOM.
- let formattedString = sb.formatStringFromName(stringName,
- Array.slice(arguments, 0),
- arguments.length);
- return PluralForm.get(aCount, formattedString);
- };
- } else {
- strings[stringName] = string.value;
- }
- }
- return strings;
-});
-
-////////////////////////////////////////////////////////////////////////////////
-//// DownloadPrompter
-
-/**
- * Allows displaying prompts related to downloads.
- *
- * @param aParent
- * The nsIDOMWindow to which prompts should be attached, or null to
- * attach prompts to the most recently active window.
- */
-function DownloadPrompter(aParent)
-{
- this._prompter = Services.ww.getNewPrompter(aParent);
-}
-
-DownloadPrompter.prototype = {
- /**
- * nsIPrompt instance for displaying messages.
- */
- _prompter: null,
-
- /**
- * Displays a warning message box that informs that the specified file is
- * executable, and asks whether the user wants to launch it. The user is
- * given the option of disabling future instances of this warning.
- *
- * @param aPath
- * String containing the full path to the file to be opened.
- *
- * @return {Promise}
- * @resolves Boolean indicating whether the launch operation can continue.
- * @rejects JavaScript exception.
- */
- confirmLaunchExecutable: function (aPath)
- {
- const kPrefAlertOnEXEOpen = "browser.download.manager.alertOnEXEOpen";
-
- try {
- try {
- if (!Services.prefs.getBoolPref(kPrefAlertOnEXEOpen)) {
- return Promise.resolve(true);
- }
- } catch (ex) {
- // If the preference does not exist, continue with the prompt.
- }
-
- let leafName = OS.Path.basename(aPath);
-
- let s = DownloadUIHelper.strings;
- let checkState = { value: false };
- let shouldLaunch = this._prompter.confirmCheck(
- s.fileExecutableSecurityWarningTitle,
- s.fileExecutableSecurityWarning(leafName, leafName),
- s.fileExecutableSecurityWarningDontAsk,
- checkState);
-
- if (shouldLaunch) {
- Services.prefs.setBoolPref(kPrefAlertOnEXEOpen, !checkState.value);
- }
-
- return Promise.resolve(shouldLaunch);
- } catch (ex) {
- return Promise.reject(ex);
- }
- },
};
From 88e4d88c1394d6902114807c7c1fc7ec122951c4 Mon Sep 17 00:00:00 2001
From: Avinash Kundaliya
Date: Fri, 16 Aug 2013 15:33:11 -0700
Subject: [PATCH 30/46] Bug 889944 - add "open in new tab" context menu item on
resources; r=harth
---
.../devtools/netmonitor/netmonitor-view.js | 12 ++++++
browser/devtools/netmonitor/netmonitor.xul | 4 ++
browser/devtools/netmonitor/test/Makefile.in | 1 +
.../test/browser_net_open_request_in_tab.js | 37 +++++++++++++++++++
.../chrome/browser/devtools/netmonitor.dtd | 10 +++++
5 files changed, 64 insertions(+)
create mode 100644 browser/devtools/netmonitor/test/browser_net_open_request_in_tab.js
diff --git a/browser/devtools/netmonitor/netmonitor-view.js b/browser/devtools/netmonitor/netmonitor-view.js
index 3e3761d9fd51..1fd9d5736281 100644
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -362,6 +362,18 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
this.selectedItem = newItem;
},
+ /**
+ * Opens selected item in a new tab.
+ */
+ openRequestInTab: function() {
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ let browser = win.getBrowser();
+
+ let selected = this.selectedItem.attachment;
+
+ browser.selectedTab = browser.addTab(selected.url);
+ },
+
/**
* Copy the request url from the currently selected item.
*/
diff --git a/browser/devtools/netmonitor/netmonitor.xul b/browser/devtools/netmonitor/netmonitor.xul
index e4a1c4138264..6d20dcdd6102 100644
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -20,6 +20,10 @@
+
- ${selector.text(__element)}
- ${selector.selectorInfo.value}
+ ${selector.sourceText}
+ ${selector.value}
diff --git a/browser/devtools/styleinspector/style-inspector.js b/browser/devtools/styleinspector/style-inspector.js
index 861c76c62e5c..55807af8d4ac 100644
--- a/browser/devtools/styleinspector/style-inspector.js
+++ b/browser/devtools/styleinspector/style-inspector.js
@@ -15,7 +15,6 @@ loader.lazyGetter(this, "RuleView", () => require("devtools/styleinspector/rule-
loader.lazyGetter(this, "ComputedView", () => require("devtools/styleinspector/computed-view"));
loader.lazyGetter(this, "_strings", () => Services.strings
.createBundle("chrome://browser/locale/devtools/styleinspector.properties"));
-loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic);
// This module doesn't currently export any symbols directly, it only
// registers inspector tools.
@@ -171,21 +170,20 @@ function ComputedViewTool(aInspector, aWindow, aIFrame)
this.window = aWindow;
this.document = aWindow.document;
this.outerIFrame = aIFrame;
- this.cssLogic = new CssLogic();
- this.view = new ComputedView.CssHtmlTree(this);
+ this.view = new ComputedView.CssHtmlTree(this, aInspector.pageStyle);
this._onSelect = this.onSelect.bind(this);
this.inspector.selection.on("detached", this._onSelect);
- this.inspector.selection.on("new-node", this._onSelect);
+ this.inspector.selection.on("new-node-front", this._onSelect);
if (this.inspector.highlighter) {
this.inspector.highlighter.on("locked", this._onSelect);
}
this.refresh = this.refresh.bind(this);
this.inspector.on("layout-change", this.refresh);
- this.inspector.sidebar.on("computedview-selected", this.refresh);
this.inspector.selection.on("pseudoclass", this.refresh);
+ this.panelSelected = this.panelSelected.bind(this);
+ this.inspector.sidebar.on("computedview-selected", this.panelSelected);
- this.cssLogic.highlight(null);
this.view.highlight(null);
this.onSelect();
@@ -196,24 +194,35 @@ exports.ComputedViewTool = ComputedViewTool;
ComputedViewTool.prototype = {
onSelect: function CVT_onSelect(aEvent)
{
+ if (!this.isActive()) {
+ // We'll try again when we're selected.
+ return;
+ }
+
+ this.view.setPageStyle(this.inspector.pageStyle);
+
if (!this.inspector.selection.isConnected() ||
!this.inspector.selection.isElementNode()) {
this.view.highlight(null);
return;
}
- if (!aEvent || aEvent == "new-node") {
+ if (!aEvent || aEvent == "new-node-front") {
if (this.inspector.selection.reason == "highlighter") {
// FIXME: We should hide view's content
} else {
- this.cssLogic.highlight(this.inspector.selection.node);
- this.view.highlight(this.inspector.selection.node);
+ let done = this.inspector.updating("computed-view");
+ this.view.highlight(this.inspector.selection.nodeFront).then(() => {
+ done();
+ });
}
}
- if (aEvent == "locked") {
- this.cssLogic.highlight(this.inspector.selection.node);
- this.view.highlight(this.inspector.selection.node);
+ if (aEvent == "locked" && this.inspector.selection.nodeFront != this.view.viewedElement) {
+ let done = this.inspector.updating("computed-view");
+ this.view.highlight(this.inspector.selection.nodeFront).then(() => {
+ done();
+ });
}
},
@@ -223,17 +232,24 @@ ComputedViewTool.prototype = {
refresh: function CVT_refresh() {
if (this.isActive()) {
- this.cssLogic.highlight(this.inspector.selection.node);
this.view.refreshPanel();
}
},
+ panelSelected: function() {
+ if (this.inspector.selection.nodeFront === this.view.viewedElement) {
+ this.view.refreshPanel();
+ } else {
+ this.onSelect();
+ }
+ },
+
destroy: function CVT_destroy(aContext)
{
this.inspector.off("layout-change", this.refresh);
this.inspector.sidebar.off("computedview-selected", this.refresh);
this.inspector.selection.off("pseudoclass", this.refresh);
- this.inspector.selection.off("new-node", this._onSelect);
+ this.inspector.selection.off("new-node-front", this._onSelect);
if (this.inspector.highlighter) {
this.inspector.highlighter.off("locked", this._onSelect);
}
diff --git a/browser/devtools/styleinspector/test/Makefile.in b/browser/devtools/styleinspector/test/Makefile.in
index 95bcc030bf35..2a6ed7bb841a 100644
--- a/browser/devtools/styleinspector/test/Makefile.in
+++ b/browser/devtools/styleinspector/test/Makefile.in
@@ -12,7 +12,6 @@ relativesrcdir = @relativesrcdir@
include $(DEPTH)/config/autoconf.mk
MOCHITEST_BROWSER_FILES = \
- browser_styleinspector.js \
browser_bug683672.js \
browser_styleinspector_bug_672746_default_styles.js \
browser_styleinspector_bug_672744_search_filter.js \
diff --git a/browser/devtools/styleinspector/test/browser_bug683672.js b/browser/devtools/styleinspector/test/browser_bug683672.js
index 0189e848fe8b..bc885c5d5daa 100644
--- a/browser/devtools/styleinspector/test/browser_bug683672.js
+++ b/browser/devtools/styleinspector/test/browser_bug683672.js
@@ -13,6 +13,7 @@ const TEST_URI = "http://example.com/browser/browser/devtools/styleinspector/tes
let tempScope = {};
let {CssHtmlTree, PropertyView} = devtools.require("devtools/styleinspector/computed-view");
+let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
function test()
{
@@ -25,49 +26,49 @@ function tabLoaded()
{
browser.removeEventListener("load", tabLoaded, true);
doc = content.document;
- openInspector(selectNode);
+ openComputedView(selectNode);
}
-function selectNode(aInspector)
+function selectNode(aInspector, aComputedView)
{
inspector = aInspector;
+ computedView = aComputedView;
div = content.document.getElementById("test");
ok(div, "captain, we have the div");
inspector.selection.setNode(div);
-
- inspector.sidebar.once("computedview-ready", function() {
- computedView = getComputedView(inspector);
-
- inspector.sidebar.select("computedview");
- runTests();
- });
+ inspector.once("inspector-updated", runTests);
}
function runTests()
{
- testMatchedSelectors();
-
- info("finishing up");
- finishUp();
+ testMatchedSelectors().then(() => {
+ info("finishing up");
+ finishUp();
+ });
}
function testMatchedSelectors()
{
info("checking selector counts, matched rules and titles");
- is(div, computedView.viewedElement,
+ is(div, computedView.viewedElement.rawNode(),
"style inspector node matches the selected node");
let propertyView = new PropertyView(computedView, "color");
- let numMatchedSelectors = propertyView.propertyInfo.matchedSelectors.length;
+ propertyView.buildMain();
+ propertyView.buildSelectorContainer();
+ propertyView.matchedExpanded = true;
+ return propertyView.refreshMatchedSelectors().then(() => {
+ let numMatchedSelectors = propertyView.matchedSelectors.length;
- is(numMatchedSelectors, 6,
- "CssLogic returns the correct number of matched selectors for div");
+ is(numMatchedSelectors, 6,
+ "CssLogic returns the correct number of matched selectors for div");
- is(propertyView.hasMatchedSelectors, true,
- "hasMatchedSelectors returns true");
+ is(propertyView.hasMatchedSelectors, true,
+ "hasMatchedSelectors returns true");
+ }).then(null, (err) => console.error(err));
}
function finishUp()
diff --git a/browser/devtools/styleinspector/test/browser_bug722196_property_view_media_queries.js b/browser/devtools/styleinspector/test/browser_bug722196_property_view_media_queries.js
index 9fe1b464a514..186d8ad6dc7f 100644
--- a/browser/devtools/styleinspector/test/browser_bug722196_property_view_media_queries.js
+++ b/browser/devtools/styleinspector/test/browser_bug722196_property_view_media_queries.js
@@ -11,6 +11,9 @@ let computedView;
const TEST_URI = "http://example.com/browser/browser/devtools/styleinspector/" +
"test/browser_bug722196_identify_media_queries.html";
+let {PropertyView} = devtools.require("devtools/styleinspector/computed-view");
+let {CssLogic} = devtools.require("devtools/styleinspector/css-logic");
+
function test()
{
waitForExplicitFinish();
@@ -23,26 +26,24 @@ function docLoaded()
browser.removeEventListener("load", docLoaded, true);
doc = content.document;
- openInspector(selectNode);
+ openComputedView(selectNode);
}
-function selectNode(aInspector)
+function selectNode(aInspector, aComputedView)
{
+ computedView = aComputedView;
+
var div = doc.querySelector("div");
ok(div, "captain, we have the div");
aInspector.selection.setNode(div);
-
- aInspector.sidebar.once("computedview-ready", function() {
- aInspector.sidebar.select("computedview");
- computedView = getComputedView(aInspector);
- checkSheets();
- });
+ aInspector.once("inspector-updated", checkCssLogic);
}
-function checkSheets()
+function checkCssLogic()
{
- let cssLogic = computedView.cssLogic;
+ let cssLogic = new CssLogic();
+ cssLogic.highlight(doc.querySelector("div"));
cssLogic.processMatchedSelectors();
let _strings = Services.strings
@@ -57,7 +58,26 @@ function checkSheets()
is(cssLogic._matchedRules[1][0].source, source2,
"rule.source gives correct output for rule 2");
- finishUp();
+ checkPropertyView();
+}
+
+function checkPropertyView()
+{
+ let propertyView = new PropertyView(computedView, "width");
+ propertyView.buildMain();
+ propertyView.buildSelectorContainer();
+ propertyView.matchedExpanded = true;
+ return propertyView.refreshMatchedSelectors().then(() => {
+ let numMatchedSelectors = propertyView.matchedSelectors.length;
+
+ is(numMatchedSelectors, 2,
+ "Property view has the correct number of matched selectors for div");
+
+ is(propertyView.hasMatchedSelectors, true,
+ "hasMatchedSelectors returns true");
+
+ finishUp();
+ }).then(null, (err) => console.error(err));
}
function finishUp()
diff --git a/browser/devtools/styleinspector/test/browser_bug_692400_element_style.js b/browser/devtools/styleinspector/test/browser_bug_692400_element_style.js
index 484ea28afdbe..2f3d464f2b58 100644
--- a/browser/devtools/styleinspector/test/browser_bug_692400_element_style.js
+++ b/browser/devtools/styleinspector/test/browser_bug_692400_element_style.js
@@ -13,29 +13,23 @@ function createDocument()
doc.title = "Style Inspector Selector Text Test";
- openInspector(openComputedView);
+ openComputedView(startTests);
}
-function openComputedView(aInspector)
+function startTests(aInspector, aComputedView)
{
+ computedView = aComputedView;
+
let div = doc.querySelector("div");
ok(div, "captain, we have the test div");
aInspector.selection.setNode(div);
-
- aInspector.sidebar.once("computedview-ready", function() {
- aInspector.sidebar.select("computedview");
- computedView = getComputedView(aInspector);
-
- Services.obs.addObserver(SI_checkText, "StyleInspector-populated", false);
- });
+ aInspector.once("inspector-updated", SI_checkText);
}
function SI_checkText()
{
- Services.obs.removeObserver(SI_checkText, "StyleInspector-populated");
-
let propertyView = null;
computedView.propertyViews.some(function(aView) {
if (aView.name == "color") {
@@ -50,15 +44,16 @@ function SI_checkText()
is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true");
propertyView.matchedExpanded = true;
- propertyView.refreshMatchedSelectors();
+ propertyView.refreshMatchedSelectors().then(() => {
- let span = propertyView.matchedSelectorsContainer.querySelector("span.rule-text");
- ok(span, "found the first table row");
+ let span = propertyView.matchedSelectorsContainer.querySelector("span.rule-text");
+ ok(span, "found the first table row");
- let selector = propertyView.matchedSelectorViews[0];
- ok(selector, "found the first matched selector view");
+ let selector = propertyView.matchedSelectorViews[0];
+ ok(selector, "found the first matched selector view");
- finishUp();
+ finishUp();
+ });
}
function finishUp()
diff --git a/browser/devtools/styleinspector/test/browser_computedview_734259_style_editor_link.js b/browser/devtools/styleinspector/test/browser_computedview_734259_style_editor_link.js
index a3091aa06da6..c4a73e4798cc 100644
--- a/browser/devtools/styleinspector/test/browser_computedview_734259_style_editor_link.js
+++ b/browser/devtools/styleinspector/test/browser_computedview_734259_style_editor_link.js
@@ -42,29 +42,20 @@ const DOCUMENT_URL = "data:text/html,"+encodeURIComponent(
-function selectNode(aInspector)
+function selectNode(aInspector, aComputedView)
{
inspector = aInspector;
+ computedView = aComputedView;
let span = doc.querySelector("span");
ok(span, "captain, we have the span");
- aInspector.selection.setNode(span);
-
- aInspector.sidebar.once("computedview-ready", function() {
- aInspector.sidebar.select("computedview");
-
- computedView = getComputedView(aInspector);
-
- Services.obs.addObserver(testInlineStyle, "StyleInspector-populated", false);
- });
+ inspector.selection.setNode(span);
+ inspector.once("inspector-updated", testInlineStyle);
}
function testInlineStyle()
{
- Services.obs.removeObserver(testInlineStyle, "StyleInspector-populated");
-
- info("expanding property");
expandProperty(0, function propertyExpanded() {
Services.ww.registerNotification(function onWindow(aSubject, aTopic) {
if (aTopic != "domwindowopened") {
@@ -137,12 +128,13 @@ function validateStyleEditorSheet(aEditor, aExpectedSheetIndex)
function expandProperty(aIndex, aCallback)
{
+ info("expanding property " + aIndex);
let contentDoc = computedView.styleDocument;
let expando = contentDoc.querySelectorAll(".expandable")[aIndex];
+
expando.click();
- // We use executeSoon to give the property time to expand.
- executeSoon(aCallback);
+ inspector.once("computed-view-property-expanded", aCallback);
}
function getLinkByIndex(aIndex)
@@ -168,7 +160,7 @@ function test()
gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee,
true);
doc = content.document;
- waitForFocus(function () { openInspector(selectNode); }, content);
+ waitForFocus(function () { openComputedView(selectNode); }, content);
}, true);
content.location = DOCUMENT_URL;
diff --git a/browser/devtools/styleinspector/test/browser_computedview_copy.js b/browser/devtools/styleinspector/test/browser_computedview_copy.js
index 67337a8e638f..f6a586e06468 100644
--- a/browser/devtools/styleinspector/test/browser_computedview_copy.js
+++ b/browser/devtools/styleinspector/test/browser_computedview_copy.js
@@ -32,33 +32,24 @@ function createDocument()
'';
doc.title = "Computed view context menu test";
- openInspector(selectNode)
+ openComputedView(selectNode)
}
-function selectNode(aInspector)
+function selectNode(aInspector, aComputedView)
{
+ computedView = aComputedView;
+ win = aInspector.sidebar.getWindowForTab("computedview");
+
let span = doc.querySelector("span");
ok(span, "captain, we have the span");
aInspector.selection.setNode(span);
-
- aInspector.sidebar.once("computedview-ready", function() {
- aInspector.sidebar.select("computedview");
-
- computedView = getComputedView(aInspector);
- win = aInspector.sidebar.getWindowForTab("computedview");
-
- Services.obs.addObserver(runStyleInspectorTests,
- "StyleInspector-populated", false);
- });
+ aInspector.once("inspector-updated", runStyleInspectorTests);
}
function runStyleInspectorTests()
{
- Services.obs.removeObserver(runStyleInspectorTests,
- "StyleInspector-populated", false);
-
let contentDocument = computedView.styleDocument;
let prop = contentDocument.querySelector(".property-view");
ok(prop, "captain, we have the property-view node");
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector.js b/browser/devtools/styleinspector/test/browser_styleinspector.js
deleted file mode 100644
index 78c67ef6a62a..000000000000
--- a/browser/devtools/styleinspector/test/browser_styleinspector.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/* 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 that the style inspector works properly
-
-let doc;
-let inspector;
-let computedView;
-
-function createDocument()
-{
- doc.body.innerHTML = '
\n' +
- '
Some header text
\n' +
- '
hi.
\n' +
- '
I am a test-case. This text exists ' +
- 'solely to provide some things to ' +
- 'highlight and count ' +
- 'style list-items in the box at right. If you are reading this, ' +
- 'you should go do something else instead. Maybe read a book. Or better ' +
- 'yet, write some test-cases for another bit of code. ' +
- 'Maybe more inspector test-cases!
\n' +
- '
end transmission
\n' +
- '
Inspect using inspectstyle(document.querySelectorAll("span")[0])
' +
- '
';
- doc.title = "Style Inspector Test";
-
- openInspector(openComputedView);
-}
-
-function openComputedView(aInspector)
-{
- inspector = aInspector;
-
- inspector.sidebar.once("computedview-ready", function() {
- computedView = getComputedView(inspector);
-
- inspector.sidebar.select("computedview");
- runStyleInspectorTests();
- });
-}
-
-function runStyleInspectorTests()
-{
- var spans = doc.querySelectorAll("span");
- ok(spans, "captain, we have the spans");
-
- for (var i = 0, numSpans = spans.length; i < numSpans; i++) {
- inspector.selection.setNode(spans[i]);
-
- is(spans[i], computedView.viewedElement,
- "style inspector node matches the selected node");
- is(computedView.viewedElement, computedView.cssLogic.viewedElement,
- "cssLogic node matches the cssHtmlTree node");
- }
-
- SI_CheckProperty();
- finishUp();
-}
-
-function SI_CheckProperty()
-{
- let cssLogic = computedView.cssLogic;
- let propertyInfo = cssLogic.getPropertyInfo("color");
- ok(propertyInfo.matchedRuleCount > 0, "color property has matching rules");
-}
-
-function finishUp()
-{
- doc = computedView = null;
- gBrowser.removeCurrentTab();
- finish();
-}
-
-function test()
-{
- waitForExplicitFinish();
- gBrowser.selectedTab = gBrowser.addTab();
- gBrowser.selectedBrowser.addEventListener("load", function(evt) {
- gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
- doc = content.document;
- waitForFocus(createDocument, content);
- }, true);
-
- content.location = "data:text/html,basic style inspector tests";
-}
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js b/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js
index 16e3aecbfaa1..9bf7bc4929ea 100644
--- a/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_672744_search_filter.js
@@ -16,24 +16,14 @@ function createDocument()
'';
doc.title = "Style Inspector Search Filter Test";
- openInspector(openComputedView);
+ openComputedView(runStyleInspectorTests);
}
-function openComputedView(aInspector)
+function runStyleInspectorTests(aInspector, aComputedView)
{
inspector = aInspector;
+ computedView = aComputedView;
- inspector.sidebar.once("computedview-ready", function() {
- inspector.sidebar.select("computedview");
- computedView = getComputedView(inspector);
-
- runStyleInspectorTests();
- });
-}
-
-function runStyleInspectorTests()
-{
- Services.obs.addObserver(SI_toggleDefaultStyles, "StyleInspector-populated", false);
SI_inspectNode();
}
@@ -43,32 +33,28 @@ function SI_inspectNode()
ok(span, "captain, we have the matches span");
inspector.selection.setNode(span);
-
- is(span, computedView.viewedElement,
- "style inspector node matches the selected node");
- is(computedView.viewedElement, computedView.cssLogic.viewedElement,
- "cssLogic node matches the cssHtmlTree node");
+ inspector.once("inspector-updated", () => {
+ is(span, computedView.viewedElement.rawNode(),
+ "style inspector node matches the selected node");
+ SI_toggleDefaultStyles();
+ }).then(null, (err) => console.error(err));
}
function SI_toggleDefaultStyles()
{
- Services.obs.removeObserver(SI_toggleDefaultStyles, "StyleInspector-populated");
-
info("checking \"Browser styles\" checkbox");
let doc = computedView.styleDocument;
let checkbox = doc.querySelector(".includebrowserstyles");
- Services.obs.addObserver(SI_AddFilterText, "StyleInspector-populated", false);
+ inspector.once("computed-view-refreshed", SI_AddFilterText);
checkbox.click();
}
function SI_AddFilterText()
{
- Services.obs.removeObserver(SI_AddFilterText, "StyleInspector-populated");
-
let doc = computedView.styleDocument;
let searchbar = doc.querySelector(".devtools-searchinput");
- Services.obs.addObserver(SI_checkFilter, "StyleInspector-populated", false);
+ inspector.once("computed-view-refreshed", SI_checkFilter);
info("setting filter text to \"color\"");
searchbar.focus();
@@ -82,7 +68,6 @@ function SI_AddFilterText()
function SI_checkFilter()
{
- Services.obs.removeObserver(SI_checkFilter, "StyleInspector-populated");
let propertyViews = computedView.propertyViews;
info("check that the correct properties are visible");
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector_bug_672746_default_styles.js b/browser/devtools/styleinspector/test/browser_styleinspector_bug_672746_default_styles.js
index 860fc7d4d5ce..869ca9303979 100644
--- a/browser/devtools/styleinspector/test/browser_styleinspector_bug_672746_default_styles.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_672746_default_styles.js
@@ -16,43 +16,27 @@ function createDocument()
'';
doc.title = "Style Inspector Default Styles Test";
- openInspector(openComputedView);
+ openComputedView(SI_inspectNode);
}
-function openComputedView(aInspector)
+function SI_inspectNode(aInspector, aComputedView)
{
inspector = aInspector;
+ computedView = aComputedView;
- inspector.sidebar.once("computedview-ready", function() {
- inspector.sidebar.select("computedview");
- computedView = getComputedView(inspector);
-
- runStyleInspectorTests();
- });
-}
-
-function runStyleInspectorTests()
-{
- Services.obs.addObserver(SI_check, "StyleInspector-populated", false);
- SI_inspectNode();
-}
-
-function SI_inspectNode()
-{
let span = doc.querySelector("#matches");
ok(span, "captain, we have the matches span");
inspector.selection.setNode(span);
-
- is(span, computedView.viewedElement,
- "style inspector node matches the selected node");
- is(computedView.viewedElement, computedView.cssLogic.viewedElement,
- "cssLogic node matches the cssHtmlTree node");
+ inspector.once("inspector-updated", () => {
+ is(span, computedView.viewedElement.rawNode(),
+ "style inspector node matches the selected node");
+ SI_check();
+ });
}
function SI_check()
{
- Services.obs.removeObserver(SI_check, "StyleInspector-populated");
is(propertyVisible("color"), true,
"span #matches color property is visible");
is(propertyVisible("background-color"), false,
@@ -66,14 +50,13 @@ function SI_toggleDefaultStyles()
// Click on the checkbox.
let doc = computedView.styleDocument;
let checkbox = doc.querySelector(".includebrowserstyles");
- Services.obs.addObserver(SI_checkDefaultStyles, "StyleInspector-populated", false);
+ inspector.once("computed-view-refreshed", SI_checkDefaultStyles);
checkbox.click();
}
function SI_checkDefaultStyles()
{
- Services.obs.removeObserver(SI_checkDefaultStyles, "StyleInspector-populated");
// Check that the default styles are now applied.
is(propertyVisible("color"), true,
"span color property is visible");
diff --git a/browser/devtools/styleinspector/test/browser_styleinspector_bug_689759_no_results_placeholder.js b/browser/devtools/styleinspector/test/browser_styleinspector_bug_689759_no_results_placeholder.js
index 0bfea29477ae..83f6e2d62052 100644
--- a/browser/devtools/styleinspector/test/browser_styleinspector_bug_689759_no_results_placeholder.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_689759_no_results_placeholder.js
@@ -15,45 +15,33 @@ function createDocument()
'Some styled text';
doc.title = "Tests that the no results placeholder works properly";
- openInspector(openComputedView);
+ openComputedView(runStyleInspectorTests);
}
-function openComputedView(aInspector)
+function runStyleInspectorTests(aInspector, aComputedView)
{
inspector = aInspector;
-
- inspector.sidebar.once("computedview-ready", function() {
- inspector.sidebar.select("computedview");
- computedView = getComputedView(inspector);
-
- runStyleInspectorTests();
- });
-}
-
-
-function runStyleInspectorTests()
-{
- Services.obs.addObserver(SI_AddFilterText, "StyleInspector-populated", false);
+ computedView = aComputedView;
let span = doc.querySelector("#matches");
ok(span, "captain, we have the matches span");
inspector.selection.setNode(span);
+ inspector.once("inspector-updated", () => {
+ is(span, computedView.viewedElement.rawNode(),
+ "style inspector node matches the selected node");
+ SI_AddFilterText();
+ });
- is(span, computedView.viewedElement,
- "style inspector node matches the selected node");
- is(computedView.viewedElement, computedView.cssLogic.viewedElement,
- "cssLogic node matches the cssHtmlTree node");
}
function SI_AddFilterText()
{
- Services.obs.removeObserver(SI_AddFilterText, "StyleInspector-populated");
-
let searchbar = computedView.searchField;
let searchTerm = "xxxxx";
- Services.obs.addObserver(SI_checkPlaceholderVisible, "StyleInspector-populated", false);
+ inspector.once("computed-view-refreshed", SI_checkPlaceholderVisible);
+
info("setting filter text to \"" + searchTerm + "\"");
searchbar.focus();
for each (let c in searchTerm) {
@@ -63,7 +51,6 @@ function SI_AddFilterText()
function SI_checkPlaceholderVisible()
{
- Services.obs.removeObserver(SI_checkPlaceholderVisible, "StyleInspector-populated");
info("SI_checkPlaceholderVisible called");
let placeholder = computedView.noResults;
let win = computedView.styleWindow;
@@ -78,7 +65,7 @@ function SI_ClearFilterText()
{
let searchbar = computedView.searchField;
- Services.obs.addObserver(SI_checkPlaceholderHidden, "StyleInspector-populated", false);
+ inspector.once("computed-view-refreshed", SI_checkPlaceholderHidden);
info("clearing filter text");
searchbar.focus();
searchbar.value = "";
@@ -87,7 +74,6 @@ function SI_ClearFilterText()
function SI_checkPlaceholderHidden()
{
- Services.obs.removeObserver(SI_checkPlaceholderHidden, "StyleInspector-populated");
let placeholder = computedView.noResults;
let win = computedView.styleWindow;
let display = win.getComputedStyle(placeholder).display;
diff --git a/browser/devtools/styleinspector/test/head.js b/browser/devtools/styleinspector/test/head.js
index 735aecb66919..37e277321393 100644
--- a/browser/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -59,6 +59,17 @@ function openRuleView(callback)
});
}
+function openComputedView(callback)
+{
+ openInspector(inspector => {
+ inspector.sidebar.once("computedview-ready", () => {
+ inspector.sidebar.select("computedview");
+ let ruleView = inspector.sidebar.getWindowForTab("computedview").computedview.view;
+ callback(inspector, ruleView);
+ })
+ });
+}
+
function addStyle(aDocument, aString)
{
let node = aDocument.createElement('style');
From 933da5b2688224ed10d801d59b86692a1dad9e58 Mon Sep 17 00:00:00 2001
From: Gaia Pushbot
Date: Sun, 18 Aug 2013 09:35:23 -0700
Subject: [PATCH 45/46] Bumping gaia.json for 2 gaia-central revision(s)
========
https://hg.mozilla.org/integration/gaia-central/rev/a7d496de30d0
Author: Etienne Segonzac
Desc: Merge pull request #11549 from tdz/bug-888799
Bug 888799: Display message if callee is busy r=etienne
========
https://hg.mozilla.org/integration/gaia-central/rev/89728ff654f3
Author: Thomas Zimmermann
Desc: Bug 888799: Display message if callee is busy
Busy calls are now signaled by error, instead of call state. This
patch adds a related message to the TelephonyHelper for informing
the user.
The old code used to play a tone for 3 seconds and close the Call
dialog afterwards. We cannot do this any longer because TonePlayer
expects the Call dialog to be around, which has already been closed
when the error gets handled.
This patch also replaces the related test of the Dialer app by a new
one. The new test verifies that the Dialer app displays the correct
error message when the number is busy.
Signed-off-by: Thomas Zimmermann
---
b2g/config/gaia.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json
index ea17d5bdb67f..41952ef7aea9 100644
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
{
- "revision": "6127157a7933badbe075f894743a09c1152afe58",
+ "revision": "a7d496de30d0a7612b31de99b01b5a4e2b749728",
"repo_path": "/integration/gaia-central"
}
From 0f40ca92e91ef99cef297800a38bc15d2b210ac0 Mon Sep 17 00:00:00 2001
From: Dave Camp
Date: Sun, 18 Aug 2013 09:55:49 -0700
Subject: [PATCH 46/46] Bug 906375 - Bind all the methods in Console.jsm's
console object (fixed patch). r=mratcliffe
---
toolkit/devtools/Console.jsm | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/toolkit/devtools/Console.jsm b/toolkit/devtools/Console.jsm
index a36eafd281c6..aa464f211dfa 100644
--- a/toolkit/devtools/Console.jsm
+++ b/toolkit/devtools/Console.jsm
@@ -589,9 +589,8 @@ function ConsoleAPI(aConsoleOptions = {}) {
// Bind all the functions to this object.
for (let prop in this) {
- let desc = Object.getOwnPropertyDescriptor(this, prop);
- if (typeof(desc.value) === "function") {
- this[prop] = desc.value.bind(this);
+ if (typeof(this[prop]) === "function") {
+ this[prop] = this[prop].bind(this);
}
}
}