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; - + +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 @@ + { + info("Starting test..."); + + let { NetMonitorView } = aMonitor.panelWin; + let { RequestsMenu } = NetMonitorView; + + RequestsMenu.lazyUpdate = false; + + waitForNetworkEvents(aMonitor, 1).then(() => { + let requestItem = RequestsMenu.getItemAtIndex(0); + RequestsMenu.selectedItem = requestItem; + + gBrowser.tabContainer.addEventListener("TabOpen", function(event) { + ok(true, "A new tab has been opened "); + removeTab(event.target); + cleanUp(); + }, false); + + RequestsMenu.openRequestInTab(); + }); + + aDebuggee.performRequests(1); + function cleanUp(){ + teardown(aMonitor).then(finish); + } + }); +} diff --git a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd index a265947962a9..38ad11fd73a7 100644 --- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd @@ -177,6 +177,16 @@ - for the Copy URL menu item displayed in the context menu for a request --> + + + + + + From 7c850ea6320cc2e8006ef47ee640c79a1f79b5ca Mon Sep 17 00:00:00 2001 From: Wes Kocher Date: Fri, 16 Aug 2013 16:09:46 -0700 Subject: [PATCH 31/46] Backed out changeset 2404f5888de3 (bug 864589) --- content/base/public/nsISelectionListener.idl | 2 -- layout/generic/nsSelection.cpp | 8 -------- mobile/android/chrome/content/SelectionHandler.js | 13 ------------- 3 files changed, 23 deletions(-) diff --git a/content/base/public/nsISelectionListener.idl b/content/base/public/nsISelectionListener.idl index 3179f141e502..9e642ee6a907 100644 --- a/content/base/public/nsISelectionListener.idl +++ b/content/base/public/nsISelectionListener.idl @@ -17,8 +17,6 @@ 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 c0f03da5249d..28fe8ff181b0 100644 --- a/layout/generic/nsSelection.cpp +++ b/layout/generic/nsSelection.cpp @@ -4444,10 +4444,6 @@ 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()); } @@ -4468,10 +4464,6 @@ 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 34be7e3546ee..7f80c1f9efb2 100644 --- a/mobile/android/chrome/content/SelectionHandler.js +++ b/mobile/android/chrome/content/SelectionHandler.js @@ -174,14 +174,6 @@ 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, @@ -211,9 +203,6 @@ 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(); @@ -474,8 +463,6 @@ 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 ce9125975a9634c9be9d3a54b8a7e64234985406 Mon Sep 17 00:00:00 2001 From: Gregory Szorc Date: Fri, 16 Aug 2013 16:57:17 -0700 Subject: [PATCH 32/46] Bug 901811 - Unify code paths for loading mozconfigs; r=ted --- python/mozbuild/mozbuild/base.py | 70 +++++++++++++----- python/mozbuild/mozbuild/test/test_base.py | 86 +++++++++++++++++++++- 2 files changed, 133 insertions(+), 23 deletions(-) diff --git a/python/mozbuild/mozbuild/base.py b/python/mozbuild/mozbuild/base.py index 43070348721e..46a1464b5128 100644 --- a/python/mozbuild/mozbuild/base.py +++ b/python/mozbuild/mozbuild/base.py @@ -57,6 +57,7 @@ class ObjdirMismatchException(BadEnvironmentException): def __str__(self): return "Objdir mismatch: %s != %s" % (self.objdir1, self.objdir2) + class MozbuildObject(ProcessExecutionMixin): """Base class providing basic functionality useful to many modules. @@ -153,16 +154,20 @@ class MozbuildObject(ProcessExecutionMixin): loader = MozconfigLoader(topsrcdir) config = loader.read_mozconfig(mozconfig) + config_topobjdir = MozbuildObject.resolve_mozconfig_topobjdir( + topsrcdir, config) + # If we're inside a objdir and the found mozconfig resolves to # another objdir, we abort. The reasoning here is that if you are # inside an objdir you probably want to perform actions on that objdir, - # not another one. - if topobjdir and config['topobjdir'] \ - and not samepath(topobjdir, config['topobjdir']): + # not another one. This prevents accidental usage of the wrong objdir + # when the current objdir is ambiguous. + if topobjdir and config_topobjdir \ + and not samepath(topobjdir, config_topobjdir): - raise ObjdirMismatchException(topobjdir, config['topobjdir']) + raise ObjdirMismatchException(topobjdir, config_topobjdir) - topobjdir = config['topobjdir'] or topobjdir + topobjdir = config_topobjdir or topobjdir if topobjdir: topobjdir = os.path.normpath(topobjdir) @@ -170,14 +175,26 @@ class MozbuildObject(ProcessExecutionMixin): # it out via config.guess. return cls(topsrcdir, None, None, topobjdir=topobjdir) + @staticmethod + def resolve_mozconfig_topobjdir(topsrcdir, mozconfig, default=None): + topobjdir = mozconfig['topobjdir'] or default + if not topobjdir: + return None + + if '@CONFIG_GUESS@' in topobjdir: + topobjdir = topobjdir.replace('@CONFIG_GUESS@', + MozbuildObject.resolve_config_guess(mozconfig, topsrcdir)) + + if not os.path.isabs(topobjdir): + topobjdir = os.path.abspath(os.path.join(topsrcdir, topobjdir)) + + return os.path.normpath(topobjdir) + @property def topobjdir(self): if self._topobjdir is None: - topobj = self.mozconfig['topobjdir'] or 'obj-@CONFIG_GUESS@' - if not os.path.isabs(topobj): - topobj = os.path.abspath(os.path.join(self.topsrcdir, topobj)) - topobj = topobj.replace("@CONFIG_GUESS@", self._config_guess) - self._topobjdir = os.path.normpath(topobj) + self._topobjdir = MozbuildObject.resolve_mozconfig_topobjdir( + self.topsrcdir, self.mozconfig, default='obj-@CONFIG_GUESS@') return self._topobjdir @@ -282,19 +299,32 @@ class MozbuildObject(ProcessExecutionMixin): return path + @staticmethod + def resolve_config_guess(mozconfig, topsrcdir): + make_extra = mozconfig['make_extra'] or [] + make_extra = dict(m.split('=', 1) for m in make_extra) + + config_guess = make_extra.get('CONFIG_GUESS', None) + + if config_guess: + return config_guess + + p = os.path.join(topsrcdir, 'build', 'autoconf', 'config.guess') + + # This is a little kludgy. We need access to the normalize_command + # function. However, that's a method of a mach mixin, so we need a + # class instance. Ideally the function should be accessible as a + # standalone function. + o = MozbuildObject(topsrcdir, None, None, None) + args = o._normalize_command([p], True) + + return subprocess.check_output(args, cwd=topsrcdir).strip() + @property def _config_guess(self): if self._config_guess_output is None: - make_extra = self.mozconfig['make_extra'] or [] - make_extra = dict(m.split('=', 1) for m in make_extra) - self._config_guess_output = make_extra.get('CONFIG_GUESS', None) - - if self._config_guess_output is None: - p = os.path.join(self.topsrcdir, 'build', 'autoconf', - 'config.guess') - args = self._normalize_command([p], True) - self._config_guess_output = subprocess.check_output(args, - cwd=self.topsrcdir).strip() + self._config_guess_output = MozbuildObject.resolve_config_guess( + self.mozconfig, self.topsrcdir) return self._config_guess_output diff --git a/python/mozbuild/mozbuild/test/test_base.py b/python/mozbuild/mozbuild/test/test_base.py index 33a354511f11..38acddedef61 100644 --- a/python/mozbuild/mozbuild/test/test_base.py +++ b/python/mozbuild/mozbuild/test/test_base.py @@ -4,8 +4,11 @@ from __future__ import unicode_literals +import json import os +import shutil import sys +import tempfile import unittest from mozfile.mozfile import NamedTemporaryFile @@ -30,6 +33,16 @@ log_manager = LoggingManager() class TestMozbuildObject(unittest.TestCase): + def setUp(self): + self._old_cwd = os.getcwd() + self._old_env = dict(os.environ) + os.environ.pop('MOZCONFIG', None) + + def tearDown(self): + os.chdir(self._old_cwd) + os.environ.clear() + os.environ.update(self._old_env) + def get_base(self): return MozbuildObject(topsrcdir, None, log_manager) @@ -45,8 +58,6 @@ class TestMozbuildObject(unittest.TestCase): self.assertTrue(os.path.isabs(base.topobjdir)) self.assertTrue(base.topobjdir.startswith(topsrcdir)) - del os.environ[b'MOZCONFIG'] - def test_objdir_trailing_slash(self): """Trailing slashes in topobjdir should be removed.""" base = self.get_base() @@ -60,7 +71,76 @@ class TestMozbuildObject(unittest.TestCase): 'foo')) self.assertTrue(base.topobjdir.endswith('foo')) - del os.environ[b'MOZCONFIG'] + @unittest.skip('Failing on buildbot.') + def test_objdir_config_status(self): + """Ensure @CONFIG_GUESS@ is handled when loading mozconfig.""" + base = self.get_base() + guess = base._config_guess + + # There may be symlinks involved, so we use real paths to ensure + # path consistency. + d = os.path.realpath(tempfile.mkdtemp()) + try: + mozconfig = os.path.join(d, 'mozconfig') + with open(mozconfig, 'wt') as fh: + fh.write('mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/foo/@CONFIG_GUESS@') + print('Wrote mozconfig %s' % mozconfig) + + topobjdir = os.path.join(d, 'foo', guess) + os.makedirs(topobjdir) + + # Create a fake topsrcdir. + guess_path = os.path.join(d, 'build', 'autoconf', 'config.guess') + os.makedirs(os.path.dirname(guess_path)) + shutil.copy(os.path.join(topsrcdir, 'build', 'autoconf', + 'config.guess',), guess_path) + + mozinfo = os.path.join(topobjdir, 'mozinfo.json') + with open(mozinfo, 'wt') as fh: + json.dump(dict( + topsrcdir=d, + mozconfig=mozconfig, + ), fh) + + os.environ[b'MOZCONFIG'] = mozconfig + os.chdir(topobjdir) + + obj = MozbuildObject.from_environment() + + self.assertEqual(obj.topobjdir, topobjdir) + finally: + shutil.rmtree(d) + + @unittest.skip('Failing on buildbot.') + def test_relative_objdir(self): + """Relative defined objdirs are loaded properly.""" + d = os.path.realpath(tempfile.mkdtemp()) + try: + mozconfig = os.path.join(d, 'mozconfig') + with open(mozconfig, 'wt') as fh: + fh.write('mk_add_options MOZ_OBJDIR=./objdir') + + topobjdir = os.path.join(d, 'objdir') + os.mkdir(topobjdir) + + mozinfo = os.path.join(topobjdir, 'mozinfo.json') + with open(mozinfo, 'wt') as fh: + json.dump(dict( + topsrcdir=d, + mozconfig=mozconfig, + ), fh) + + os.environ[b'MOZCONFIG'] = mozconfig + child = os.path.join(topobjdir, 'foo', 'bar') + os.makedirs(child) + os.chdir(child) + + obj = MozbuildObject.from_environment() + + self.assertEqual(obj.topobjdir, topobjdir) + + finally: + shutil.rmtree(d) def test_config_guess(self): # It's difficult to test for exact values from the output of From 328160be666707686c875a8b48c8fa120bf7dce4 Mon Sep 17 00:00:00 2001 From: Sam Foster Date: Fri, 16 Aug 2013 16:56:50 -0700 Subject: [PATCH 33/46] Bug 904317 - observe viewstate in view markup, call _adjustDOMforViewState at init and from onViewStateChange. r=rsilveira --HG-- extra : rebase_source : accadb9ffa0a8cb3026ddb5aff0952cfd8a5186c --- .../base/content/startui/BookmarksView.js | 1 + .../metro/base/content/startui/HistoryView.js | 2 ++ .../base/content/startui/RemoteTabsView.js | 1 + browser/metro/base/content/startui/Start.xul | 8 +++--- browser/metro/base/content/startui/StartUI.js | 8 +++++- .../base/content/startui/TopSitesView.js | 28 ++++++++++++++----- browser/metro/modules/View.jsm | 9 ++++-- 7 files changed, 43 insertions(+), 14 deletions(-) diff --git a/browser/metro/base/content/startui/BookmarksView.js b/browser/metro/base/content/startui/BookmarksView.js index 911e374d9601..a3cf34279e7c 100644 --- a/browser/metro/base/content/startui/BookmarksView.js +++ b/browser/metro/base/content/startui/BookmarksView.js @@ -30,6 +30,7 @@ function BookmarksView(aSet, aLimit, aRoot, aFilterUnpinned) { StartUI.chromeWin.addEventListener('BookmarksNeedsRefresh', this, false); window.addEventListener("TabClose", this, true); + this._adjustDOMforViewState(); this.root = aRoot; } diff --git a/browser/metro/base/content/startui/HistoryView.js b/browser/metro/base/content/startui/HistoryView.js index 9a2b8207b697..42b100541ce1 100644 --- a/browser/metro/base/content/startui/HistoryView.js +++ b/browser/metro/base/content/startui/HistoryView.js @@ -20,6 +20,8 @@ function HistoryView(aSet, aLimit, aFilterUnpinned) { StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); StartUI.chromeWin.addEventListener('HistoryNeedsRefresh', this, false); window.addEventListener("TabClose", this, true); + + this._adjustDOMforViewState(); } HistoryView.prototype = Util.extend(Object.create(View.prototype), { diff --git a/browser/metro/base/content/startui/RemoteTabsView.js b/browser/metro/base/content/startui/RemoteTabsView.js index 6e49d54925e2..6cd321b0f4fc 100644 --- a/browser/metro/base/content/startui/RemoteTabsView.js +++ b/browser/metro/base/content/startui/RemoteTabsView.js @@ -37,6 +37,7 @@ function RemoteTabsView(aSet, aSetUIAccessList) { else { this.setUIAccessVisible(false); } + this._adjustDOMforViewState(); } RemoteTabsView.prototype = Util.extend(Object.create(View.prototype), { diff --git a/browser/metro/base/content/startui/Start.xul b/browser/metro/base/content/startui/Start.xul index 1a9b152ac96e..e01a5c91237a 100644 --- a/browser/metro/base/content/startui/Start.xul +++ b/browser/metro/base/content/startui/Start.xul @@ -45,7 +45,7 @@ &narrowTopSitesHeader.label; - + @@ -53,7 +53,7 @@ &narrowBookmarksHeader.label; - + @@ -61,7 +61,7 @@ &narrowRecentHistoryHeader.label; - + #ifdef MOZ_SERVICES_SYNC @@ -70,7 +70,7 @@ &narrowRemoteTabsHeader.label; - + #endif diff --git a/browser/metro/base/content/startui/StartUI.js b/browser/metro/base/content/startui/StartUI.js index e88f9b65f7a6..0647dbec4d29 100644 --- a/browser/metro/base/content/startui/StartUI.js +++ b/browser/metro/base/content/startui/StartUI.js @@ -13,7 +13,7 @@ const kBottomContentMargin = 50; var StartUI = { get startUI() { return document.getElementById("start-container"); }, - get maxResultsPerSection() { + get maxResultsPerSection() { return Services.prefs.getIntPref("browser.display.startUI.maxresults"); }, @@ -31,6 +31,7 @@ var StartUI = { this.chromeWin.InputSourceHelper.isPrecise ? "precise" : "imprecise"); this._updateStartHeight(); + this._adjustDOMforViewState(); TopSitesStartView.init(); BookmarksStartView.init(); @@ -145,6 +146,11 @@ var StartUI = { break; } document.getElementById("bcast_windowState").setAttribute("viewstate", currViewState); + if (currViewState == "snapped") { + document.getElementById("start-topsites-grid").removeAttribute("tiletype"); + } else { + document.getElementById("start-topsites-grid").setAttribute("tiletype", "thumbnail"); + } } }, diff --git a/browser/metro/base/content/startui/TopSitesView.js b/browser/metro/base/content/startui/TopSitesView.js index 38f27e7ce8af..eebf9b9b319a 100644 --- a/browser/metro/base/content/startui/TopSitesView.js +++ b/browser/metro/base/content/startui/TopSitesView.js @@ -25,6 +25,8 @@ function TopSitesView(aGrid, aMaxSites) { Services.obs.addObserver(this, "Metro:RefreshTopsiteThumbnail", false); Services.obs.addObserver(this, "metro_viewstate_changed", false); + this._adjustDOMforViewState(); + NewTabUtils.allPages.register(this); TopSites.prepareCache().then(function(){ this.populateGrid(); @@ -224,6 +226,25 @@ TopSitesView.prototype = Util.extend(Object.create(View.prototype), { return prefs.getBoolPref("browser.firstrun.show.localepicker"); }, + _adjustDOMforViewState: function _adjustDOMforViewState(aState) { + if (!this._set) + return; + if (!aState) + aState = this._set.getAttribute("viewstate"); + + View.prototype._adjustDOMforViewState.call(this, aState); + + // propogate tiletype changes down to tile children + let tileType = this._set.getAttribute("tiletype"); + for (let item of this._set.children) { + if (tileType) { + item.setAttribute("tiletype", tileType); + } else { + item.removeAttribute("tiletype"); + } + } + }, + // nsIObservers observe: function (aSubject, aTopic, aState) { switch(aTopic) { @@ -232,13 +253,6 @@ TopSitesView.prototype = Util.extend(Object.create(View.prototype), { break; case "metro_viewstate_changed": this.onViewStateChange(aState); - for (let item of this._set.children) { - if (aState == "snapped") { - item.removeAttribute("tiletype"); - } else { - item.setAttribute("tiletype", "thumbnail"); - } - } break; } }, diff --git a/browser/metro/modules/View.jsm b/browser/metro/modules/View.jsm index 5e3efdc1aff4..d9968bb25c4c 100644 --- a/browser/metro/modules/View.jsm +++ b/browser/metro/modules/View.jsm @@ -27,13 +27,18 @@ function View() { } View.prototype = { - - onViewStateChange: function (aState) { + _adjustDOMforViewState: function _adjustDOMforViewState(aState) { if (this._set) { + if (undefined == aState) + aState = this._set.getAttribute("viewstate"); this._set.setAttribute("suppressonselect", (aState == "snapped")); } }, + onViewStateChange: function (aState) { + this._adjustDOMforViewState(aState); + }, + _updateFavicon: function pv__updateFavicon(aItem, aUri) { if ("string" == typeof aUri) { aUri = makeURI(aUri); From ee2d964bb5df21d6d969ab6abaa7dcb1ce47f433 Mon Sep 17 00:00:00 2001 From: Mark Capella Date: Fri, 16 Aug 2013 21:51:41 -0400 Subject: [PATCH 34/46] Bug 864589 - Show/hide text selection handles if a selection is programatically added/removed, r=margaret, ehsan --- content/base/public/nsISelectionListener.idl | 4 +++- layout/generic/nsSelection.cpp | 8 ++++++++ mobile/android/chrome/content/SelectionHandler.js | 13 +++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/content/base/public/nsISelectionListener.idl b/content/base/public/nsISelectionListener.idl index 9e642ee6a907..43f61d6607da 100644 --- a/content/base/public/nsISelectionListener.idl +++ b/content/base/public/nsISelectionListener.idl @@ -8,7 +8,7 @@ interface nsIDOMDocument; interface nsISelection; -[scriptable, uuid(A6CF90E2-15B3-11d2-932E-00805F8ADD32)] +[scriptable, uuid(280cd784-23c2-468d-8624-354e0b3804bd)] interface nsISelectionListener : nsISupports { const short NO_REASON=0; @@ -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 a8337233b1595ad9bbca7390f7372c6efe261003 Mon Sep 17 00:00:00 2001 From: Phil Ringnalda Date: Fri, 16 Aug 2013 19:04:25 -0700 Subject: [PATCH 35/46] Back out ae91affad44f (bug 889944) for browser-chrome bustage CLOSED TREE --- .../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 deletions(-) delete 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 1fd9d5736281..3e3761d9fd51 100644 --- a/browser/devtools/netmonitor/netmonitor-view.js +++ b/browser/devtools/netmonitor/netmonitor-view.js @@ -362,18 +362,6 @@ 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 6d20dcdd6102..e4a1c4138264 100644 --- a/browser/devtools/netmonitor/netmonitor.xul +++ b/browser/devtools/netmonitor/netmonitor.xul @@ -20,10 +20,6 @@ - { - info("Starting test..."); - - let { NetMonitorView } = aMonitor.panelWin; - let { RequestsMenu } = NetMonitorView; - - RequestsMenu.lazyUpdate = false; - - waitForNetworkEvents(aMonitor, 1).then(() => { - let requestItem = RequestsMenu.getItemAtIndex(0); - RequestsMenu.selectedItem = requestItem; - - gBrowser.tabContainer.addEventListener("TabOpen", function(event) { - ok(true, "A new tab has been opened "); - removeTab(event.target); - cleanUp(); - }, false); - - RequestsMenu.openRequestInTab(); - }); - - aDebuggee.performRequests(1); - function cleanUp(){ - teardown(aMonitor).then(finish); - } - }); -} diff --git a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd index 38ad11fd73a7..a265947962a9 100644 --- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd @@ -177,16 +177,6 @@ - for the Copy URL menu item displayed in the context menu for a request --> - - - - - - From 0768589e3fb4c85958fb602a96ea80e2b64867f8 Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Sat, 17 Aug 2013 13:24:30 +0200 Subject: [PATCH 36/46] Bug 879375 - Adapt reflow exclusion list entry for SessionStore.getWindowDimensions() to recent changes; r=me/test-only --- browser/base/content/test/browser_tabopen_reflows.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/browser/base/content/test/browser_tabopen_reflows.js b/browser/base/content/test/browser_tabopen_reflows.js index d28c6304a31d..2bfc7d655ca1 100644 --- a/browser/base/content/test/browser_tabopen_reflows.js +++ b/browser/base/content/test/browser_tabopen_reflows.js @@ -45,13 +45,7 @@ const EXPECTED_REFLOWS = [ "ssi_getWindowDimension@resource:///modules/sessionstore/SessionStore.jsm|" + "@resource:///modules/sessionstore/SessionStore.jsm|" + "ssi_updateWindowFeatures@resource:///modules/sessionstore/SessionStore.jsm|" + - "ssi_collectWindowData@resource:///modules/sessionstore/SessionStore.jsm|" + - "@resource:///modules/sessionstore/SessionStore.jsm|" + - "ssi_forEachBrowserWindow@resource:///modules/sessionstore/SessionStore.jsm|" + - "ssi_getCurrentState@resource:///modules/sessionstore/SessionStore.jsm|" + - "ssi_saveState@resource:///modules/sessionstore/SessionStore.jsm|" + - "ssi_onTimerCallback@resource:///modules/sessionstore/SessionStore.jsm|" + - "ssi_observe@resource:///modules/sessionstore/SessionStore.jsm|", + "ssi_collectWindowData@resource:///modules/sessionstore/SessionStore.jsm|", // tabPreviews.capture() "tabPreviews_capture@chrome://browser/content/browser.js|" + From 88be90eef4c93911c471df1f9eef10f62912bd62 Mon Sep 17 00:00:00 2001 From: Paolo Amadini Date: Sat, 17 Aug 2013 15:13:10 +0200 Subject: [PATCH 37/46] Bug 896927 - Handle the executable warning prompt. r=enn --- .../downloads/src/DownloadsCommon.jsm | 89 +++++-------- .../jsdownloads/src/DownloadIntegration.jsm | 27 +++- .../jsdownloads/src/DownloadUIHelper.jsm | 125 ++++++++++++++++++ 3 files changed, 186 insertions(+), 55 deletions(-) diff --git a/browser/components/downloads/src/DownloadsCommon.jsm b/browser/components/downloads/src/DownloadsCommon.jsm index e9722019a9ae..03a0d4291f96 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", @@ -480,65 +482,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); }, /** 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..f044231d5858 100644 --- a/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm +++ b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm @@ -24,6 +24,20 @@ 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://mozapps/locale/downloads/downloads.properties"; + +const kStringsRequiringFormatting = { + fileExecutableSecurityWarning: true, +}; + //////////////////////////////////////////////////////////////////////////////// //// DownloadUIHelper @@ -31,4 +45,115 @@ 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 { + 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 7173348258798b5c1c18ded097ef6c101535c83a Mon Sep 17 00:00:00 2001 From: Neil Deakin Date: Sat, 17 Aug 2013 15:19:45 +0200 Subject: [PATCH 38/46] Bug 901262 - Add a DownloadList method to remove stopped downloads. r=paolo --- .../jsdownloads/src/DownloadList.jsm | 42 +++++++--------- .../test/unit/test_DownloadList.js | 49 ++++++++++++++++++- 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/toolkit/components/jsdownloads/src/DownloadList.jsm b/toolkit/components/jsdownloads/src/DownloadList.jsm index 41cbff15dc52..25a23c1df3a2 100644 --- a/toolkit/components/jsdownloads/src/DownloadList.jsm +++ b/toolkit/components/jsdownloads/src/DownloadList.jsm @@ -205,19 +205,28 @@ DownloadList.prototype = { }, /** - * Removes downloads from the list based on the given test function. + * Removes downloads from the list that have finished, have failed, or have + * been canceled without keeping partial data. A filter function may be + * specified to remove only a subset of those downloads. * - * @param aTestFn - * The test function. + * This method finalizes each removed download, ensuring that any partially + * downloaded data associated with it is also removed. + * + * @param aFilterFn + * The filter function is called with each download as its only + * argument, and should return true to remove the download and false + * to keep it. This parameter may be null or omitted to have no + * additional filter. */ - _removeWhere: function DL__removeWhere(aTestFn) { + removeFinished: function DL_removeFinished(aFilterFn) { Task.spawn(function() { let list = yield this.getAll(); for (let download of list) { // Remove downloads that have been canceled, even if the cancellation // operation hasn't completed yet so we don't check "stopped" here. - if ((download.succeeded || download.canceled || download.error) && - aTestFn(download)) { + // Failed downloads with partial data are also removed. + if (download.stopped && (!download.hasPartialData || download.error) && + (!aFilterFn || aFilterFn(download))) { // Remove the download first, so that the views don't get the change // notifications that may occur during finalization. this.remove(download); @@ -225,25 +234,12 @@ DownloadList.prototype = { // This works even if the download state has changed meanwhile. We // don't need to wait for the procedure to be complete before // processing the other downloads in the list. - download.finalize(true); + download.finalize(true).then(null, Cu.reportError); } } }.bind(this)).then(null, Cu.reportError); }, - /** - * Removes downloads within the given period of time. - * - * @param aStartTime - * The start time date object. - * @param aEndTime - * The end time date object. - */ - removeByTimeframe: function DL_removeByTimeframe(aStartTime, aEndTime) { - this._removeWhere(download => download.startTime >= aStartTime && - download.startTime <= aEndTime); - }, - //////////////////////////////////////////////////////////////////////////// //// nsISupports @@ -253,12 +249,12 @@ DownloadList.prototype = { //// nsINavHistoryObserver onDeleteURI: function DL_onDeleteURI(aURI, aGUID) { - this._removeWhere(download => aURI.equals(NetUtil.newURI( - download.source.url))); + this.removeFinished(download => aURI.equals(NetUtil.newURI( + download.source.url))); }, onClearHistory: function DL_onClearHistory() { - this._removeWhere(() => true); + this.removeFinished(); }, onTitleChanged: function () {}, diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js index aeaca97f53c2..209808112c36 100644 --- a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js @@ -239,7 +239,7 @@ add_task(function test_history_expiration() // Start download two and then cancel it. downloadTwo.start(); - let promiseCanceled = downloadTwo.cancel(); + yield downloadTwo.cancel(); // Force a history expiration. let expire = Cc["@mozilla.org/places/expiration;1"] @@ -247,7 +247,6 @@ add_task(function test_history_expiration() expire.observe(null, "places-debug-start-expiration", -1); yield deferred.promise; - yield promiseCanceled; cleanup(); }); @@ -285,3 +284,49 @@ add_task(function test_history_clear() yield deferred.promise; }); + +/** + * Tests the removeFinished method to ensure that it only removes + * finished downloads. + */ +add_task(function test_removeFinished() +{ + let list = yield promiseNewDownloadList(); + let downloadOne = yield promiseNewDownload(); + let downloadTwo = yield promiseNewDownload(); + let downloadThree = yield promiseNewDownload(); + let downloadFour = yield promiseNewDownload(); + list.add(downloadOne); + list.add(downloadTwo); + list.add(downloadThree); + list.add(downloadFour); + + let deferred = Promise.defer(); + let removeNotifications = 0; + let downloadView = { + onDownloadRemoved: function (aDownload) { + do_check_true(aDownload == downloadOne || + aDownload == downloadTwo || + aDownload == downloadThree); + do_check_true(removeNotifications < 3); + if (++removeNotifications == 3) { + deferred.resolve(); + } + }, + }; + list.addView(downloadView); + + // Start three of the downloads, but don't start downloadTwo, then set + // downloadFour to have partial data. All downloads except downloadFour + // should be removed. + yield downloadOne.start(); + yield downloadThree.start(); + yield downloadFour.start(); + downloadFour.hasPartialData = true; + + list.removeFinished(); + yield deferred.promise; + + let downloads = yield list.getAll() + do_check_eq(downloads.length, 1); +}); From c1b7543f1b7e7c20794028a1d4b29962e37a7f92 Mon Sep 17 00:00:00 2001 From: Neil Deakin Date: Sat, 17 Aug 2013 15:19:55 +0200 Subject: [PATCH 39/46] Bug 899125 - Part 1 of 2 - Allow using the JavaScript API instead of nsIDownloadManager when clearing recent history. r=paolo --- browser/base/content/sanitize.js | 72 +++++++++++++++++--------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/browser/base/content/sanitize.js b/browser/base/content/sanitize.js index 541f430da453..771783e69eba 100644 --- a/browser/base/content/sanitize.js +++ b/browser/base/content/sanitize.js @@ -8,9 +8,15 @@ XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", "resource://gre/modules/FormHistory.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/commonjs/sdk/core/promise.js"); - +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", + "resource:///modules/DownloadsCommon.jsm"); + function Sanitizer() {} Sanitizer.prototype = { // warning to the caller: this one may raise an exception (e.g. bug #265028) @@ -303,47 +309,47 @@ Sanitizer.prototype = { downloads: { clear: function () { - var dlMgr = Components.classes["@mozilla.org/download-manager;1"] - .getService(Components.interfaces.nsIDownloadManager); + if (DownloadsCommon.useJSTransfer) { + Task.spawn(function () { + let filterByTime = this.range ? + (download => download.startTime >= this.range[0] && + download.startTime <= this.range[1]) : null; - var dlsToRemove = []; - if (this.range) { - // First, remove the completed/cancelled downloads - dlMgr.removeDownloadsByTimeframe(this.range[0], this.range[1]); + // Clear all completed/cancelled downloads + let publicList = yield Downloads.getPublicDownloadList(); + publicList.removeFinished(filterByTime); - // Queue up any active downloads that started in the time span as well - for (let dlsEnum of [dlMgr.activeDownloads, dlMgr.activePrivateDownloads]) { - while (dlsEnum.hasMoreElements()) { - var dl = dlsEnum.next(); - if (dl.startTime >= this.range[0]) - dlsToRemove.push(dl); - } - } + let privateList = yield Downloads.getPrivateDownloadList(); + privateList.removeFinished(filterByTime); + }.bind(this)).then(null, Cu.reportError); } else { - // Clear all completed/cancelled downloads - dlMgr.cleanUp(); - dlMgr.cleanUpPrivate(); - - // Queue up all active ones as well - for (let dlsEnum of [dlMgr.activeDownloads, dlMgr.activePrivateDownloads]) { - while (dlsEnum.hasMoreElements()) { - dlsToRemove.push(dlsEnum.next()); - } + var dlMgr = Components.classes["@mozilla.org/download-manager;1"] + .getService(Components.interfaces.nsIDownloadManager); + + if (this.range) { + // First, remove the completed/cancelled downloads + dlMgr.removeDownloadsByTimeframe(this.range[0], this.range[1]); + } + else { + // Clear all completed/cancelled downloads + dlMgr.cleanUp(); + dlMgr.cleanUpPrivate(); } } - - // Remove any queued up active downloads - dlsToRemove.forEach(function (dl) { - dl.remove(); - }); }, - get canClear() + canClear : function(aCallback, aArg) { - var dlMgr = Components.classes["@mozilla.org/download-manager;1"] - .getService(Components.interfaces.nsIDownloadManager); - return dlMgr.canCleanUp || dlMgr.canCleanUpPrivate; + if (DownloadsCommon.useJSTransfer) { + aCallback("downloads", true, aArg); + } + else { + var dlMgr = Components.classes["@mozilla.org/download-manager;1"] + .getService(Components.interfaces.nsIDownloadManager); + aCallback("downloads", dlMgr.canCleanUp || dlMgr.canCleanUpPrivate, aArg); + } + return false; } }, From f70e33580c3be2a036166f768ed5eb19d20d2022 Mon Sep 17 00:00:00 2001 From: Neil Deakin Date: Sat, 17 Aug 2013 15:20:01 +0200 Subject: [PATCH 40/46] Bug 899125 - Part 2 of 2 - Allow using the JavaScript API instead of nsIDownloadManager when forgetting about sites. r=paolo --- toolkit/forgetaboutsite/ForgetAboutSite.jsm | 99 +++++++++++++-------- 1 file changed, 60 insertions(+), 39 deletions(-) diff --git a/toolkit/forgetaboutsite/ForgetAboutSite.jsm b/toolkit/forgetaboutsite/ForgetAboutSite.jsm index 1fd5b2271c67..5e834f0c7b37 100644 --- a/toolkit/forgetaboutsite/ForgetAboutSite.jsm +++ b/toolkit/forgetaboutsite/ForgetAboutSite.jsm @@ -4,8 +4,12 @@ Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); this.EXPORTED_SYMBOLS = ["ForgetAboutSite"]; @@ -92,50 +96,67 @@ this.ForgetAboutSite = { } // Downloads - let (dm = Cc["@mozilla.org/download-manager;1"]. - getService(Ci.nsIDownloadManager)) { - // Active downloads - for (let enumerator of [dm.activeDownloads, dm.activePrivateDownloads]) { - while (enumerator.hasMoreElements()) { - let dl = enumerator.getNext().QueryInterface(Ci.nsIDownload); - if (hasRootDomain(dl.source.host, aDomain)) { - dl.cancel(); - dl.remove(); + let useJSTransfer = false; + try { + useJSTransfer = Services.prefs.getBoolPref("browser.download.useJSTransfer"); + } catch(ex) { } + + if (useJSTransfer) { + Task.spawn(function() { + for (let promiseList of [Downloads.getPublicDownloadList(), + Downloads.getPrivateDownloadList()]) { + let list = yield promiseList; + list.removeFinished(download => hasRootDomain( + NetUtil.newURI(download.source.url).host, aDomain)); + } + }).then(null, Cu.reportError); + } + else { + let (dm = Cc["@mozilla.org/download-manager;1"]. + getService(Ci.nsIDownloadManager)) { + // Active downloads + for (let enumerator of [dm.activeDownloads, dm.activePrivateDownloads]) { + while (enumerator.hasMoreElements()) { + let dl = enumerator.getNext().QueryInterface(Ci.nsIDownload); + if (hasRootDomain(dl.source.host, aDomain)) { + dl.cancel(); + dl.remove(); + } } } - } - function deleteAllLike(db) { - // NOTE: This is lossy, but we feel that it is OK to be lossy here and not - // invoke the cost of creating a URI for each download entry and - // ensure that the hostname matches. - let stmt = db.createStatement( - "DELETE FROM moz_downloads " + - "WHERE source LIKE ?1 ESCAPE '/' " + - "AND state NOT IN (?2, ?3, ?4)" - ); - let pattern = stmt.escapeStringForLIKE(aDomain, "/"); - stmt.bindByIndex(0, "%" + pattern + "%"); - stmt.bindByIndex(1, Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING); - stmt.bindByIndex(2, Ci.nsIDownloadManager.DOWNLOAD_PAUSED); - stmt.bindByIndex(3, Ci.nsIDownloadManager.DOWNLOAD_QUEUED); - try { - stmt.execute(); - } - finally { - stmt.finalize(); + function deleteAllLike(db) { + // NOTE: This is lossy, but we feel that it is OK to be lossy here and not + // invoke the cost of creating a URI for each download entry and + // ensure that the hostname matches. + let stmt = db.createStatement( + "DELETE FROM moz_downloads " + + "WHERE source LIKE ?1 ESCAPE '/' " + + "AND state NOT IN (?2, ?3, ?4)" + ); + let pattern = stmt.escapeStringForLIKE(aDomain, "/"); + stmt.bindByIndex(0, "%" + pattern + "%"); + stmt.bindByIndex(1, Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING); + stmt.bindByIndex(2, Ci.nsIDownloadManager.DOWNLOAD_PAUSED); + stmt.bindByIndex(3, Ci.nsIDownloadManager.DOWNLOAD_QUEUED); + try { + stmt.execute(); + } + finally { + stmt.finalize(); + } } + + // Completed downloads + deleteAllLike(dm.DBConnection); + deleteAllLike(dm.privateDBConnection); + + // We want to rebuild the list if the UI is showing, so dispatch the + // observer topic + let os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + os.notifyObservers(null, "download-manager-remove-download", null); } - - // Completed downloads - deleteAllLike(dm.DBConnection); - deleteAllLike(dm.privateDBConnection); - - // We want to rebuild the list if the UI is showing, so dispatch the - // observer topic - let os = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - os.notifyObservers(null, "download-manager-remove-download", null); } // Passwords From a01c5c7e8c22543d2ae108d16a34e9b7967953c6 Mon Sep 17 00:00:00 2001 From: Dave Camp Date: Fri, 16 Aug 2013 16:45:05 -0700 Subject: [PATCH 41/46] Bug 906375 - Bind all the methods in Console.jsm's console object. r=mratcliffe --- toolkit/devtools/Console.jsm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/toolkit/devtools/Console.jsm b/toolkit/devtools/Console.jsm index 53abd3d25503..a36eafd281c6 100644 --- a/toolkit/devtools/Console.jsm +++ b/toolkit/devtools/Console.jsm @@ -586,6 +586,14 @@ function ConsoleAPI(aConsoleOptions = {}) { this.dump = aConsoleOptions.dump || dump; this.prefix = aConsoleOptions.prefix || ""; this.maxLogLevel = aConsoleOptions.maxLogLevel || "all"; + + // 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); + } + } } ConsoleAPI.prototype = { From 083c360416f77ebe03bb11e093e29d181affc5fd Mon Sep 17 00:00:00 2001 From: Dave Camp Date: Thu, 8 Aug 2013 08:44:56 -0700 Subject: [PATCH 42/46] Bug 903573 - Get a reference to the style actor in the inspector panel. r=mratcliffe --- browser/devtools/inspector/inspector-panel.js | 21 ++++++++++++------- browser/devtools/inspector/selection.js | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/browser/devtools/inspector/inspector-panel.js b/browser/devtools/inspector/inspector-panel.js index ee446cacdfda..aabec8d52ea1 100644 --- a/browser/devtools/inspector/inspector-panel.js +++ b/browser/devtools/inspector/inspector-panel.js @@ -45,12 +45,8 @@ InspectorPanel.prototype = { */ open: function InspectorPanel_open() { return this.target.makeRemote().then(() => { - return this.target.inspector.getWalker(); - }).then(walker => { - if (this._destroyPromise) { - walker.release().then(null, console.error); - } - this.walker = walker; + return this._getWalker(); + }).then(() => { return this._getDefaultNodeForSelection(); }).then(defaultSelection => { return this._deferredOpen(defaultSelection); @@ -149,6 +145,16 @@ InspectorPanel.prototype = { return deferred.promise; }, + _getWalker: function() { + let inspector = this.target.inspector; + return inspector.getWalker().then(walker => { + this.walker = walker; + return inspector.getPageStyle(); + }).then(pageStyle => { + this.pageStyle = pageStyle; + }); + }, + /** * Return a promise that will resolve to the default node for selection. */ @@ -317,7 +323,7 @@ InspectorPanel.prototype = { try { selfUpdate(selection); } catch(ex) { - console.error(ex); + console.error(ex) } }, Ci.nsIThread.DISPATCH_NORMAL); }, @@ -403,6 +409,7 @@ InspectorPanel.prototype = { if (this.walker) { this._destroyPromise = this.walker.release().then(null, console.error); delete this.walker; + delete this.pageStyle; } else { this._destroyPromise = promise.resolve(null); } diff --git a/browser/devtools/inspector/selection.js b/browser/devtools/inspector/selection.js index 62c4da6da417..929df15deda9 100644 --- a/browser/devtools/inspector/selection.js +++ b/browser/devtools/inspector/selection.js @@ -81,7 +81,7 @@ Selection.prototype = { attributeChange = true; } if (m.type == "childList") { - if (!detached && !this.isConnected()) { + if (!detached && this.isNode() && !this.isConnected()) { parentNode = m.target; detached = true; } From 7b3a1264e17ceac87c3c9232b47339422b2cf691 Mon Sep 17 00:00:00 2001 From: Dave Camp Date: Thu, 8 Aug 2013 08:44:57 -0700 Subject: [PATCH 43/46] Bug 886038 - Port the rule view to the styles actor. r=mratcliffe --- .../browser_inspector_pseudoclass_lock.js | 8 +- .../test/browser_responsiveruleview.js | 16 +- .../devtools/responsivedesign/test/head.js | 10 + browser/devtools/styleinspector/rule-view.js | 391 ++++++++++++------ .../styleinspector/style-inspector.js | 83 ++-- ...owser_bug722196_rule_view_media_queries.js | 15 +- .../browser_bug722691_rule_view_increment.js | 46 +-- ...s_property_completion_existing_property.js | 2 +- ...65_css_property_completion_new_property.js | 2 +- ...completion_existing_property_value_pair.js | 2 +- ...alue_completion_new_property_value_pair.js | 2 +- ...owser_ruleview_734259_style_editor_link.js | 46 +-- .../test/browser_ruleview_copy.js | 7 +- .../browser_ruleview_editor_changedvalues.js | 109 +++-- .../test/browser_ruleview_focus.js | 18 +- .../test/browser_ruleview_inherit.js | 79 ++-- .../test/browser_ruleview_manipulation.js | 77 ++-- .../test/browser_ruleview_override.js | 135 +++--- .../test/browser_ruleview_ui.js | 170 ++++---- .../test/browser_ruleview_update.js | 140 +++---- ...tyleinspector_bug_677930_urls_clickable.js | 78 ++-- browser/devtools/styleinspector/test/head.js | 34 ++ 22 files changed, 830 insertions(+), 640 deletions(-) diff --git a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js index 14e1df05e650..579daec2ee8d 100644 --- a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js +++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js @@ -68,14 +68,12 @@ function performTests() // inspector has been told of the pseudoclass lock change. inspector.selection.once("pseudoclass", () => { // Give the rule view time to update. - executeSoon(() => { + inspector.once("rule-view-refreshed", () => { testAdded(); - - // toggle the lock off and wait for the pseudoclass event again. + // Change the pseudo class and give the rule view time to update. inspector.togglePseudoClass(pseudo); inspector.selection.once("pseudoclass", () => { - // Give the rule view time to update. - executeSoon(() => { + inspector.once("rule-view-refreshed", () => { testRemoved(); testRemovedFromUI(); diff --git a/browser/devtools/responsivedesign/test/browser_responsiveruleview.js b/browser/devtools/responsivedesign/test/browser_responsiveruleview.js index 9aeb3147a303..db50bd836ebe 100644 --- a/browser/devtools/responsivedesign/test/browser_responsiveruleview.js +++ b/browser/devtools/responsivedesign/test/browser_responsiveruleview.js @@ -48,26 +48,20 @@ function test() { instance.setSize(500, 500); - openInspector(onInspectorUIOpen); + openRuleView(onInspectorUIOpen); } - function onInspectorUIOpen(aInspector) { + function onInspectorUIOpen(aInspector, aRuleView) { inspector = aInspector; + ruleView = aRuleView; ok(inspector, "Got inspector instance"); - inspector.sidebar.select("ruleview"); let div = content.document.getElementsByTagName("div")[0]; - - inspector.sidebar.once("ruleview-ready", function() { - Services.obs.addObserver(testShrink, "StyleInspector-populated", false); - inspector.selection.setNode(div); - }); + inspector.selection.setNode(div); + inspector.once("inspector-updated", testShrink); } function testShrink() { - Services.obs.removeObserver(testShrink, "StyleInspector-populated"); - - ruleView = inspector.sidebar.getWindowForTab("ruleview").ruleview.view; is(numberOfRules(), 2, "Should have two rules initially."); diff --git a/browser/devtools/responsivedesign/test/head.js b/browser/devtools/responsivedesign/test/head.js index c357f1a8a7a7..5aff2b1f050c 100644 --- a/browser/devtools/responsivedesign/test/head.js +++ b/browser/devtools/responsivedesign/test/head.js @@ -18,3 +18,13 @@ function openInspector(callback) }); } +function openRuleView(callback) +{ + openInspector(inspector => { + inspector.sidebar.once("ruleview-ready", () => { + inspector.sidebar.select("ruleview"); + let ruleView = inspector.sidebar.getWindowForTab("ruleview").ruleview.view; + callback(inspector, ruleView); + }) + }); +} diff --git a/browser/devtools/styleinspector/rule-view.js b/browser/devtools/styleinspector/rule-view.js index dda9bb63356d..ca429a78683f 100644 --- a/browser/devtools/styleinspector/rule-view.js +++ b/browser/devtools/styleinspector/rule-view.js @@ -7,9 +7,11 @@ "use strict"; const {Cc, Ci, Cu} = require("chrome"); +const promise = require("sdk/core/promise"); let {CssLogic} = require("devtools/styleinspector/css-logic"); let {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor"); +let {ELEMENT_STYLE} = require("devtools/server/actors/styles"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -34,6 +36,46 @@ const CSS_RESOURCE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/; const IOService = Cc["@mozilla.org/network/io-service;1"] .getService(Ci.nsIIOService); +function promiseWarn(err) { + console.error(err); + return promise.reject(err); +} + +/** + * To figure out how shorthand properties are interpreted by the + * engine, we will set properties on a dummy element and observe + * how their .style attribute reflects them as computed values. + * This function creates the document in which those dummy elements + * will be created. + */ +var gDummyPromise; +function createDummyDocument() { + if (gDummyPromise) { + return gDummyPromise; + } + const { getDocShell, create: makeFrame } = require("sdk/frame/utils"); + + let frame = makeFrame(Services.appShell.hiddenDOMWindow.document, { + nodeName: "iframe", + namespaceURI: "http://www.w3.org/1999/xhtml", + allowJavascript: false, + allowPlugins: false, + allowAuth: false + }); + let docShell = getDocShell(frame); + let eventTarget = docShell.chromeEventHandler; + docShell.createAboutBlankContentViewer(Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal)); + let window = docShell.contentViewer.DOMDocument.defaultView; + window.location = "data:text/html,"; + let deferred = promise.defer() + eventTarget.addEventListener("DOMContentLoaded", function handler(event) { + eventTarget.removeEventListener("DOMContentLoaded", handler, false); + deferred.resolve(window.document); + }, false); + gDummyPromise = deferred.promise; + return gDummyPromise; +} + /** * Our model looks like this: * @@ -62,13 +104,17 @@ const IOService = Cc["@mozilla.org/network/io-service;1"] * The ElementStyle can use this object to store metadata * that might outlast the rule view, particularly the current * set of disabled properties. + * @param {PageStyleFront} aPageStyle + * Front for the page style actor that will be providing + * the style information. * * @constructor */ -function ElementStyle(aElement, aStore) +function ElementStyle(aElement, aStore, aPageStyle) { this.element = aElement; this.store = aStore || {}; + this.pageStyle = aPageStyle; // We don't want to overwrite this.store.userProperties so we only create it // if it doesn't already exist. @@ -85,9 +131,12 @@ function ElementStyle(aElement, aStore) // To figure out how shorthand properties are interpreted by the // engine, we will set properties on a dummy element and observe // how their .style attribute reflects them as computed values. - this.dummyElement = doc.createElementNS(this.element.namespaceURI, - this.element.tagName); - this.populate(); + this.dummyElementPromise = createDummyDocument().then(document => { + this.dummyElement = document.createElementNS(this.element.namespaceURI, + this.element.tagName); + document.documentElement.appendChild(this.dummyElement); + return this.dummyElement; + }).then(null, promiseWarn); } // We're exporting _ElementStyle for unit tests. exports._ElementStyle = ElementStyle; @@ -101,6 +150,17 @@ ElementStyle.prototype = { // to figure out how shorthand properties will be parsed. dummyElement: null, + destroy: function() + { + this.dummyElement = null; + this.dummyElementPromise.then(dummyElement => { + if (dummyElement.parentNode) { + dummyElement.parentNode.removeChild(dummyElement); + } + this.dummyElementPromise = null; + }); + }, + /** * Called by the Rule object when it has been changed through the * setProperty* methods. @@ -115,62 +175,44 @@ ElementStyle.prototype = { /** * Refresh the list of rules to be displayed for the active element. * Upon completion, this.rules[] will hold a list of Rule objects. + * + * Returns a promise that will be resolved when the elementStyle is + * ready. */ populate: function ElementStyle_populate() { - // Store the current list of rules (if any) during the population - // process. They will be reused if possible. - this._refreshRules = this.rules; + let populated = this.pageStyle.getApplied(this.element, { + inherited: true, + matchedSelectors: true + }).then(entries => { + // Make sure the dummy element has been created before continuing... + return this.dummyElementPromise.then(() => { + if (this.populated != populated) { + // Don't care anymore. + return promise.reject("unused"); + } - this.rules = []; + // Store the current list of rules (if any) during the population + // process. They will be reused if possible. + this._refreshRules = this.rules; - let element = this.element; - do { - this._addElementRules(element); - } while ((element = element.parentNode) && - element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE); + this.rules = []; - // Mark overridden computed styles. - this.markOverridden(); + for (let entry of entries) { + this._maybeAddRule(entry); + } - // We're done with the previous list of rules. - delete this._refreshRules; - }, + // Mark overridden computed styles. + this.markOverridden(); - _addElementRules: function ElementStyle_addElementRules(aElement) - { - let inherited = aElement !== this.element ? aElement : null; + // We're done with the previous list of rules. + delete this._refreshRules; - // Include the element's style first. - this._maybeAddRule({ - style: aElement.style, - selectorText: CssLogic.l10n("rule.sourceElement"), - inherited: inherited - }); - - // Get the styles that apply to the element. - var domRules = domUtils.getCSSStyleRules(aElement); - - // getCSStyleRules returns ordered from least-specific to - // most-specific. - for (let i = domRules.Count() - 1; i >= 0; i--) { - let domRule = domRules.GetElementAt(i); - - // XXX: Optionally provide access to system sheets. - let contentSheet = CssLogic.isContentStylesheet(domRule.parentStyleSheet); - if (!contentSheet) { - continue; - } - - if (domRule.type !== Ci.nsIDOMCSSRule.STYLE_RULE) { - continue; - } - - this._maybeAddRule({ - domRule: domRule, - inherited: inherited + return null; }); - } + }).then(null, promiseWarn); + this.populated = populated; + return this.populated; }, /** @@ -186,8 +228,12 @@ ElementStyle.prototype = { { // If we've already included this domRule (for example, when a // common selector is inherited), ignore it. - if (aOptions.domRule && - this.rules.some(function(rule) rule.domRule === aOptions.domRule)) { + if (aOptions.rule && + this.rules.some(function(rule) rule.domRule === aOptions.rule)) { + return false; + } + + if (aOptions.system) { return false; } @@ -195,11 +241,13 @@ ElementStyle.prototype = { // If we're refreshing and the rule previously existed, reuse the // Rule object. - for (let r of (this._refreshRules || [])) { - if (r.matches(aOptions)) { - rule = r; - rule.refresh(); - break; + if (this._refreshRules) { + for (let r of this._refreshRules) { + if (r.matches(aOptions)) { + rule = r; + rule.refresh(aOptions); + break; + } } } @@ -214,6 +262,7 @@ ElementStyle.prototype = { } this.rules.push(rule); + return true; }, /** @@ -324,11 +373,7 @@ ElementStyle.prototype = { * The ElementStyle to which this rule belongs. * @param {object} aOptions * The information used to construct this rule. Properties include: - * domRule: the nsIDOMCSSStyleRule to view, if any. - * style: the nsIDOMCSSStyleDeclaration to view. If omitted, - * the domRule's style will be used. - * selectorText: selector text to display. If omitted, the domRule's - * selectorText will be used. + * rule: A StyleRuleActor * inherited: An element this rule was inherited from. If omitted, * the rule applies directly to the current element. * @constructor @@ -336,15 +381,17 @@ ElementStyle.prototype = { function Rule(aElementStyle, aOptions) { this.elementStyle = aElementStyle; - this.domRule = aOptions.domRule || null; - this.style = aOptions.style || this.domRule.style; - this.selectorText = aOptions.selectorText || this.domRule.selectorText; + this.domRule = aOptions.rule || null; + this.style = aOptions.rule; + this.matchedSelectors = aOptions.matchedSelectors || []; + this.inherited = aOptions.inherited || null; + this._modificationDepth = 0; if (this.domRule) { let parentRule = this.domRule.parentRule; if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) { - this.mediaText = parentRule.media.mediaText; + this.mediaText = parentRule.mediaText; } } @@ -363,11 +410,12 @@ Rule.prototype = { return this._title; } this._title = CssLogic.shortSource(this.sheet); - if (this.domRule) { + if (this.domRule.type !== ELEMENT_STYLE) { this._title += ":" + this.ruleLine; } - return this._title + (this.mediaText ? " @media " + this.mediaText : ""); + this._title = this._title + (this.mediaText ? " @media " + this.mediaText : ""); + return this._title; }, get inheritedSource() @@ -387,6 +435,11 @@ Rule.prototype = { return this._inheritedSource; }, + get selectorText() + { + return this.domRule.selectors ? this.domRule.selectors.join(", ") : CssLogic.l10n("rule.sourceElement"); + }, + /** * The rule's stylesheet. */ @@ -400,11 +453,7 @@ Rule.prototype = { */ get ruleLine() { - if (!this.sheet) { - // No stylesheet, no ruleLine - return null; - } - return domUtils.getRuleLine(this.domRule); + return this.domRule ? this.domRule.line : null; }, /** @@ -416,7 +465,7 @@ Rule.prototype = { */ matches: function Rule_matches(aOptions) { - return (this.style === (aOptions.style || aOptions.domRule.style)); + return this.style === aOptions.rule; }, /** @@ -447,12 +496,15 @@ Rule.prototype = { * when calling from setPropertyValue & setPropertyName to signify * that the property should be saved in store.userProperties. */ - applyProperties: function Rule_applyProperties(aName) + applyProperties: function Rule_applyProperties(aModifications, aName) { + if (!aModifications) { + aModifications = this.style.startModifyingProperties(); + } let disabledProps = []; let store = this.elementStyle.store; - for each (let prop in this.textProps) { + for (let prop of this.textProps) { if (!prop.enabled) { disabledProps.push({ name: prop.name, @@ -462,21 +514,11 @@ Rule.prototype = { continue; } - this.style.setProperty(prop.name, prop.value, prop.priority); + aModifications.setProperty(prop.name, prop.value, prop.priority); - if (aName && prop.name == aName) { - store.userProperties.setProperty( - this.style, prop.name, - this.style.getPropertyValue(prop.name), - prop.value); - } - // Refresh the property's priority from the style, to reflect - // any changes made during parsing. - prop.priority = this.style.getPropertyPriority(prop.name); prop.updateComputed(); } - this.elementStyle._changed(); // Store disabled properties in the disabled store. let disabled = this.elementStyle.store.disabled; @@ -486,7 +528,46 @@ Rule.prototype = { disabled.delete(this.style); } - this.elementStyle.markOverridden(); + let promise = aModifications.apply().then(() => { + let cssProps = {}; + for (let cssProp of this._parseCSSText(this.style.cssText)) { + cssProps[cssProp.name] = cssProp; + } + + for (let textProp of this.textProps) { + if (!textProp.enabled) { + continue; + } + let cssProp = cssProps[textProp.name]; + + if (!cssProp) { + cssProp = { + name: textProp.name, + value: "", + priority: "" + } + } + + if (aName && textProp.name == aName) { + store.userProperties.setProperty( + this.style, textProp.name, + null, + cssProp.value, + textProp.value); + } + textProp.priority = cssProp.priority; + } + + this.elementStyle.markOverridden(); + + if (promise === this._applyingModifications) { + this._applyingModifications = null; + } + + this.elementStyle._changed(); + }).then(null, promiseWarn); + this._applyingModifications = promise; + return promise; }, /** @@ -502,9 +583,10 @@ Rule.prototype = { if (aName === aProperty.name) { return; } - this.style.removeProperty(aProperty.name); + let modifications = this.style.startModifyingProperties(); + modifications.removeProperty(aProperty.name); aProperty.name = aName; - this.applyProperties(aName); + this.applyProperties(modifications, aName); }, /** @@ -524,7 +606,7 @@ Rule.prototype = { } aProperty.value = aValue; aProperty.priority = aPriority; - this.applyProperties(aProperty.name); + this.applyProperties(null, aProperty.name); }, /** @@ -533,10 +615,11 @@ Rule.prototype = { setPropertyEnabled: function Rule_enableProperty(aProperty, aValue) { aProperty.enabled = !!aValue; + let modifications = this.style.startModifyingProperties(); if (!aProperty.enabled) { - this.style.removeProperty(aProperty.name); + modifications.removeProperty(aProperty.name); } - this.applyProperties(); + this.applyProperties(modifications); }, /** @@ -546,10 +629,32 @@ Rule.prototype = { removeProperty: function Rule_removeProperty(aProperty) { this.textProps = this.textProps.filter(function(prop) prop != aProperty); - this.style.removeProperty(aProperty); + let modifications = this.style.startModifyingProperties(); + modifications.removeProperty(aProperty.name); // Need to re-apply properties in case removing this TextProperty // exposes another one. - this.applyProperties(); + this.applyProperties(modifications); + }, + + _parseCSSText: function Rule_parseProperties(aCssText) + { + let lines = aCssText.match(CSS_LINE_RE); + let props = []; + + for (let line of lines) { + dump("line: " + line + "\n"); + let [, name, value, priority] = CSS_PROP_RE.exec(line) || [] + if (!name || !value) { + continue; + } + + props.push({ + name: name, + value: value, + priority: priority || "" + }); + } + return props; }, /** @@ -560,19 +665,15 @@ Rule.prototype = { { let textProps = []; let store = this.elementStyle.store; - let lines = this.style.cssText.match(CSS_LINE_RE); - for each (let line in lines) { - let matches = CSS_PROP_RE.exec(line); - if (!matches || !matches[2]) - continue; - - let name = matches[1]; + let props = this._parseCSSText(this.style.cssText); + for (let prop of props) { + let name = prop.name; if (this.inherited && !domUtils.isInheritedProperty(name)) { continue; } - let value = store.userProperties.getProperty(this.style, name, matches[2]); - let prop = new TextProperty(this, name, value, matches[3] || ""); - textProps.push(prop); + let value = store.userProperties.getProperty(this.style, name, prop.value); + let textProp = new TextProperty(this, name, value, prop.priority); + textProps.push(textProp); } return textProps; @@ -607,8 +708,9 @@ Rule.prototype = { * Reread the current state of the rules and rebuild text * properties as needed. */ - refresh: function Rule_refresh() + refresh: function Rule_refresh(aOptions) { + this.matchedSelectors = aOptions.matchedSelectors || []; let newTextProps = this._getTextProperties(); // Update current properties for each property present on the style. @@ -861,14 +963,15 @@ TextProperty.prototype = { * The CSS rule view can use this object to store metadata * that might outlast the rule view, particularly the current * set of disabled properties. - * @param {