diff --git a/accessible/src/jsat/AccessFu.jsm b/accessible/src/jsat/AccessFu.jsm index 89b3ec7ad40e..023b16ae3032 100644 --- a/accessible/src/jsat/AccessFu.jsm +++ b/accessible/src/jsat/AccessFu.jsm @@ -31,9 +31,8 @@ this.AccessFu = { Utils.init(aWindow); try { - let bridgeCc = Cc['@mozilla.org/android/bridge;1']; - bridgeCc.getService(Ci.nsIAndroidBridge).handleGeckoMessage( - JSON.stringify({ type: 'Accessibility:Ready' })); + Services.androidBridge.handleGeckoMessage( + { type: 'Accessibility:Ready' }); Services.obs.addObserver(this, 'Accessibility:Settings', false); } catch (x) { // Not on Android @@ -702,8 +701,7 @@ var Output = { get androidBridge() { delete this.androidBridge; if (Utils.MozBuildApp === 'mobile/android') { - this.androidBridge = Cc['@mozilla.org/android/bridge;1'].getService( - Ci.nsIAndroidBridge); + this.androidBridge = Services.androidBridge; } else { this.androidBridge = null; } @@ -734,7 +732,7 @@ var Output = { androidEvent.brailleOutput = this.brailleState.init(androidEvent.brailleOutput); break; } - this.androidBridge.handleGeckoMessage(JSON.stringify(androidEvent)); + this.androidBridge.handleGeckoMessage(androidEvent); } }, @@ -902,9 +900,8 @@ var Input = { if (Utils.MozBuildApp == 'mobile/android') // Return focus to native Android browser chrome. - Cc['@mozilla.org/android/bridge;1']. - getService(Ci.nsIAndroidBridge).handleGeckoMessage( - JSON.stringify({ type: 'ToggleChrome:Focus' })); + Services.androidBridge.handleGeckoMessage( + { type: 'ToggleChrome:Focus' }); break; case aEvent.DOM_VK_RETURN: if (this.editState.editing) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index e51c98effb3b..0a62db436ac2 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1228,7 +1228,7 @@ pref("devtools.webconsole.filter.csserror", true); pref("devtools.webconsole.filter.cssparser", false); pref("devtools.webconsole.filter.csslog", false); pref("devtools.webconsole.filter.exception", true); -pref("devtools.webconsole.filter.jswarn", false); +pref("devtools.webconsole.filter.jswarn", true); pref("devtools.webconsole.filter.jslog", false); pref("devtools.webconsole.filter.error", true); pref("devtools.webconsole.filter.warn", true); diff --git a/browser/base/content/abouthome/aboutHome.css b/browser/base/content/abouthome/aboutHome.css index c2703180fc10..50a1ad30a055 100644 --- a/browser/base/content/abouthome/aboutHome.css +++ b/browser/base/content/abouthome/aboutHome.css @@ -386,6 +386,11 @@ body[narrow] #restorePreviousSession::before { transform-origin: 0 0; } + .launchButton:-moz-dir(rtl)::before, + #aboutMozilla:-moz-dir(rtl)::before { + transform: scale(.5) translateX(32px); + } + #downloads::before { content: url("chrome://browser/content/abouthome/downloads@2x.png"); } @@ -422,6 +427,11 @@ body[narrow] #restorePreviousSession::before { content: url("chrome://browser/content/abouthome/restore@2x.png"); } + #restorePreviousSession:-moz-dir(rtl)::before { + transform: scale(-0.5, 0.5) translateX(24px); + transform-origin: top center; + } + #aboutMozilla::before { content: url("chrome://browser/content/abouthome/mozilla@2x.png"); } diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js index d53c60948d06..7612d1ee3923 100644 --- a/browser/base/content/browser-places.js +++ b/browser/base/content/browser-places.js @@ -884,6 +884,10 @@ let PlacesToolbarHelper = { if (!viewElt || viewElt._placesView) return; + // CustomizableUI.addListener is idempotent, so we can safely + // call this multiple times. + CustomizableUI.addListener(this); + // If the bookmarks toolbar item is: // - not in a toolbar, or; // - the toolbar is collapsed, or; @@ -899,7 +903,11 @@ let PlacesToolbarHelper = { if (forceToolbarOverflowCheck) { viewElt._placesView.updateOverflowStatus(); } - this.customizeChange(); + this._setupPlaceholder(); + }, + + uninit: function PTH_uninit() { + CustomizableUI.removeListener(this); }, customizeStart: function PTH_customizeStart() { @@ -914,10 +922,15 @@ let PlacesToolbarHelper = { }, customizeChange: function PTH_customizeChange() { + this._setupPlaceholder(); + }, + + _setupPlaceholder: function PTH_setupPlaceholder() { let placeholder = this._placeholder; if (!placeholder) { return; } + let shouldWrapNow = this._getShouldWrap(); if (this._shouldWrap != shouldWrapNow) { if (shouldWrapNow) { @@ -958,7 +971,40 @@ let PlacesToolbarHelper = { element = element.parentNode; } return null; - } + }, + + onWidgetUnderflow: function(aNode, aContainer) { + // The view gets broken by being removed and reinserted by the overflowable + // toolbar, so we have to force an uninit and reinit. + let win = aNode.ownerDocument.defaultView; + if (aNode.id == "personal-bookmarks" && win == window) { + this._resetView(); + } + }, + + onWidgetAdded: function(aWidgetId, aArea, aPosition) { + if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) { + // It's possible (with the "Add to Menu", "Add to Toolbar" context + // options) that the Places Toolbar Items have been moved without + // letting us prepare and handle it with with customizeStart and + // customizeDone. If that's the case, we need to reset the views + // since they're probably broken from the DOM reparenting. + this._resetView(); + } + }, + + _resetView: function() { + if (this._viewElt) { + // It's possible that the placesView might not exist, and we need to + // do a full init. This could happen if the Bookmarks Toolbar Items are + // moved to the Menu Panel, and then to the toolbar with the "Add to Toolbar" + // context menu option, outside of customize mode. + if (this._viewElt._placesView) { + this._viewElt._placesView.uninit(); + } + this.init(true); + } + }, }; //////////////////////////////////////////////////////////////////////////////// @@ -1164,6 +1210,16 @@ let BookmarkingUI = { // so kill current view and let popupshowing generate a new one. if (this.button._placesView) this.button._placesView.uninit(); + + // We have to do the same thing for the "special" views underneath the + // the bookmarks menu. + const kSpecialViewNodeIDs = ["BMB_bookmarksToolbar", "BMB_unsortedBookmarks"]; + for (let viewNodeID of kSpecialViewNodeIDs) { + let elem = document.getElementById(viewNodeID); + if (elem && elem._placesView) { + elem._placesView.uninit(); + } + } }, onCustomizeStart: function BUI_customizeStart(aWindow) { diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 985d6fe20e7a..5c75e8fd35c4 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -1289,6 +1289,8 @@ var gBrowserInit = { } catch (ex) { } + PlacesToolbarHelper.uninit(); + BookmarkingUI.uninit(); TabsInTitlebar.uninit(); diff --git a/browser/base/content/newtab/sites.js b/browser/base/content/newtab/sites.js index 5545b66ee56e..c5a157d4b71f 100644 --- a/browser/base/content/newtab/sites.js +++ b/browser/base/content/newtab/sites.js @@ -154,7 +154,9 @@ Site.prototype = { */ refreshThumbnail: function Site_refreshThumbnail() { let thumbnail = this._querySelector(".newtab-thumbnail"); - thumbnail.style.backgroundColor = this.link.bgColor; + if (this.link.bgColor) { + thumbnail.style.backgroundColor = this.link.bgColor; + } let uri = this.link.imageURISpec || PageThumbs.getThumbnailURL(this.url); thumbnail.style.backgroundImage = "url(" + uri + ")"; }, diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 9086e875eedd..80e63ce23959 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -424,7 +424,7 @@ newPrompt.clientTop; // style flush to assure binding is attached - let tab = self._getTabForContentWindow(browser.contentWindow); + let tab = self._getTabForBrowser(browser); newPrompt.init(args, tab, onCloseCallback); return newPrompt; }, @@ -3196,7 +3196,13 @@ // We're about to open a modal dialog, make sure the opening // tab is brought to the front. - this.selectedTab = this._getTabForContentWindow(event.target.top); + // If this is a same-process modal dialog, then we're given its DOM + // window as the event's target. For remote dialogs, we're given the + // browser, but that's in the originalTarget. + // XXX Why originalTarget for the browser? + this.selectedTab = (event.target instanceof Window) ? + this._getTabForContentWindow(event.target.top) : + this._getTabForBrowser(event.originalTarget); ]]> diff --git a/browser/components/customizableui/test/browser.ini b/browser/components/customizableui/test/browser.ini index 8f3e00125264..823041175974 100644 --- a/browser/components/customizableui/test/browser.ini +++ b/browser/components/customizableui/test/browser.ini @@ -93,6 +93,10 @@ skip-if = os == "linux" [browser_978084_dragEnd_after_move.js] [browser_980155_add_overflow_toolbar.js] [browser_981418-widget-onbeforecreated-handler.js] + +[browser_984455_bookmarks_items_reparenting.js] +skip-if = os == "linux" + [browser_985815_propagate_setToolbarVisibility.js] [browser_981305_separator_insertion.js] [browser_987177_destroyWidget_xul.js] diff --git a/browser/components/customizableui/test/browser_880164_customization_context_menus.js b/browser/components/customizableui/test/browser_880164_customization_context_menus.js index a336fb9578d6..4dd2486d9d39 100644 --- a/browser/components/customizableui/test/browser_880164_customization_context_menus.js +++ b/browser/components/customizableui/test/browser_880164_customization_context_menus.js @@ -10,7 +10,7 @@ const isOSX = (Services.appinfo.OS === "Darwin"); // show a context menu with options to move it. add_task(function() { let contextMenu = document.getElementById("toolbar-context-menu"); - let shownPromise = contextMenuShown(contextMenu); + let shownPromise = popupShown(contextMenu); let homeButton = document.getElementById("home-button"); EventUtils.synthesizeMouse(homeButton, 2, 2, {type: "contextmenu", button: 2 }); yield shownPromise; @@ -30,7 +30,7 @@ add_task(function() { ); checkContextMenu(contextMenu, expectedEntries); - let hiddenPromise = contextMenuHidden(contextMenu); + let hiddenPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenPromise; }); @@ -40,7 +40,7 @@ add_task(function() { // and a toggle option for the extra toolbar add_task(function() { let contextMenu = document.getElementById("toolbar-context-menu"); - let shownPromise = contextMenuShown(contextMenu); + let shownPromise = popupShown(contextMenu); let toolbar = createToolbarWithPlacements("880164_empty_toolbar", []); toolbar.setAttribute("context", "toolbar-context-menu"); toolbar.setAttribute("toolbarname", "Fancy Toolbar for Context Menu"); @@ -63,7 +63,7 @@ add_task(function() { ); checkContextMenu(contextMenu, expectedEntries); - let hiddenPromise = contextMenuHidden(contextMenu); + let hiddenPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenPromise; removeCustomToolbars(); @@ -74,7 +74,7 @@ add_task(function() { // show a context menu with disabled options to move it. add_task(function() { let contextMenu = document.getElementById("toolbar-context-menu"); - let shownPromise = contextMenuShown(contextMenu); + let shownPromise = popupShown(contextMenu); let urlBarContainer = document.getElementById("urlbar-container"); // Need to make sure not to click within an edit field. let urlbarRect = urlBarContainer.getBoundingClientRect(); @@ -96,7 +96,7 @@ add_task(function() { ); checkContextMenu(contextMenu, expectedEntries); - let hiddenPromise = contextMenuHidden(contextMenu); + let hiddenPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenPromise; }); @@ -135,7 +135,7 @@ add_task(function() { yield shownPanelPromise; let contextMenu = document.getElementById("customizationPanelItemContextMenu"); - let shownContextPromise = contextMenuShown(contextMenu); + let shownContextPromise = popupShown(contextMenu); let newWindowButton = document.getElementById("new-window-button"); ok(newWindowButton, "new-window-button was found"); EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2}); @@ -151,7 +151,7 @@ add_task(function() { ]; checkContextMenu(contextMenu, expectedEntries); - let hiddenContextPromise = contextMenuHidden(contextMenu); + let hiddenContextPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenContextPromise; @@ -165,7 +165,7 @@ add_task(function() { add_task(function() { yield startCustomizing(); let contextMenu = document.getElementById("toolbar-context-menu"); - let shownPromise = contextMenuShown(contextMenu); + let shownPromise = popupShown(contextMenu); let homeButton = document.getElementById("wrapper-home-button"); EventUtils.synthesizeMouse(homeButton, 2, 2, {type: "contextmenu", button: 2}); yield shownPromise; @@ -185,7 +185,7 @@ add_task(function() { ); checkContextMenu(contextMenu, expectedEntries); - let hiddenContextPromise = contextMenuHidden(contextMenu); + let hiddenContextPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenContextPromise; }); @@ -194,7 +194,7 @@ add_task(function() { // show a context menu with options to move it. add_task(function() { let contextMenu = document.getElementById("customizationPaletteItemContextMenu"); - let shownPromise = contextMenuShown(contextMenu); + let shownPromise = popupShown(contextMenu); let openFileButton = document.getElementById("wrapper-open-file-button"); EventUtils.synthesizeMouse(openFileButton, 2, 2, {type: "contextmenu", button: 2}); yield shownPromise; @@ -205,7 +205,7 @@ add_task(function() { ]; checkContextMenu(contextMenu, expectedEntries); - let hiddenContextPromise = contextMenuHidden(contextMenu); + let hiddenContextPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenContextPromise; }); @@ -214,7 +214,7 @@ add_task(function() { // should show a context menu with options to move it. add_task(function() { let contextMenu = document.getElementById("customizationPanelItemContextMenu"); - let shownPromise = contextMenuShown(contextMenu); + let shownPromise = popupShown(contextMenu); let newWindowButton = document.getElementById("wrapper-new-window-button"); EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2}); yield shownPromise; @@ -227,7 +227,7 @@ add_task(function() { ]; checkContextMenu(contextMenu, expectedEntries); - let hiddenContextPromise = contextMenuHidden(contextMenu); + let hiddenContextPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenContextPromise; yield endCustomizing(); @@ -241,7 +241,7 @@ add_task(function() { yield startCustomizing(this.otherWin); let contextMenu = this.otherWin.document.getElementById("customizationPanelItemContextMenu"); - let shownPromise = contextMenuShown(contextMenu); + let shownPromise = popupShown(contextMenu); let newWindowButton = this.otherWin.document.getElementById("wrapper-new-window-button"); EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2}, this.otherWin); yield shownPromise; @@ -254,7 +254,7 @@ add_task(function() { ]; checkContextMenu(contextMenu, expectedEntries, this.otherWin); - let hiddenContextPromise = contextMenuHidden(contextMenu); + let hiddenContextPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenContextPromise; yield endCustomizing(this.otherWin); @@ -267,13 +267,13 @@ add_task(function() { add_task(function() { yield startCustomizing(); let contextMenu = document.getElementById("customizationPanelItemContextMenu"); - let shownPromise = contextMenuShown(contextMenu); + let shownPromise = popupShown(contextMenu); let zoomControls = document.getElementById("wrapper-zoom-controls"); EventUtils.synthesizeMouse(zoomControls, 2, 2, {type: "contextmenu", button: 2}); yield shownPromise; // Execute the command to move the item from the panel to the toolbar. contextMenu.childNodes[0].doCommand(); - let hiddenPromise = contextMenuHidden(contextMenu); + let hiddenPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenPromise; yield endCustomizing(); @@ -282,7 +282,7 @@ add_task(function() { is(zoomControls.parentNode.id, "nav-bar-customization-target", "Zoom-controls should be on the nav-bar"); contextMenu = document.getElementById("toolbar-context-menu"); - shownPromise = contextMenuShown(contextMenu); + shownPromise = popupShown(contextMenu); EventUtils.synthesizeMouse(zoomControls, 2, 2, {type: "contextmenu", button: 2}); yield shownPromise; @@ -301,7 +301,7 @@ add_task(function() { ); checkContextMenu(contextMenu, expectedEntries); - hiddenPromise = contextMenuHidden(contextMenu); + hiddenPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenPromise; yield resetCustomization(); @@ -315,7 +315,7 @@ add_task(function() { yield PanelUI.show(); let contextMenu = document.getElementById("customizationPanelItemContextMenu"); - let shownContextPromise = contextMenuShown(contextMenu); + let shownContextPromise = popupShown(contextMenu); let newWindowButton = document.getElementById("new-window-button"); ok(newWindowButton, "new-window-button was found"); EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2}); @@ -331,7 +331,7 @@ add_task(function() { ]; checkContextMenu(contextMenu, expectedEntries); - let hiddenContextPromise = contextMenuHidden(contextMenu); + let hiddenContextPromise = popupHidden(contextMenu); contextMenu.hidePopup(); yield hiddenContextPromise; diff --git a/browser/components/customizableui/test/browser_884402_customize_from_overflow.js b/browser/components/customizableui/test/browser_884402_customize_from_overflow.js index c56bc90aee2d..a7fc5d620996 100644 --- a/browser/components/customizableui/test/browser_884402_customize_from_overflow.js +++ b/browser/components/customizableui/test/browser_884402_customize_from_overflow.js @@ -27,7 +27,7 @@ add_task(function() { yield shownPanelPromise; let contextMenu = document.getElementById("toolbar-context-menu"); - let shownContextPromise = contextMenuShown(contextMenu); + let shownContextPromise = popupShown(contextMenu); let homeButton = document.getElementById("home-button"); ok(homeButton, "home-button was found"); is(homeButton.getAttribute("overflowedItem"), "true", "Home button is overflowing"); @@ -51,7 +51,7 @@ add_task(function() { ); checkContextMenu(contextMenu, expectedEntries); - let hiddenContextPromise = contextMenuHidden(contextMenu); + let hiddenContextPromise = popupHidden(contextMenu); let hiddenPromise = promisePanelElementHidden(window, overflowPanel); let moveToPanel = contextMenu.querySelector(".customize-context-moveToPanel"); if (moveToPanel) { diff --git a/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js b/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js new file mode 100644 index 000000000000..ceb82ea8a05f --- /dev/null +++ b/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js @@ -0,0 +1,256 @@ +/* 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"; + +let gNavBar = document.getElementById(CustomizableUI.AREA_NAVBAR); +let gOverflowList = document.getElementById(gNavBar.getAttribute("overflowtarget")); + +const kBookmarksButton = "bookmarks-menu-button"; +const kBookmarksItems = "personal-bookmarks"; +const kOriginalWindowWidth = window.outerWidth; +const kSmallWidth = 400; + +/** + * Helper function that opens the bookmarks menu, and returns a Promise that + * resolves as soon as the menu is ready for interaction. + */ +function bookmarksMenuPanelShown() { + let deferred = Promise.defer(); + let bookmarksMenuPopup = document.getElementById("BMB_bookmarksPopup"); + let onTransitionEnd = (e) => { + if (e.target == bookmarksMenuPopup) { + bookmarksMenuPopup.removeEventListener("transitionend", onTransitionEnd); + deferred.resolve(); + } + } + bookmarksMenuPopup.addEventListener("transitionend", onTransitionEnd); + return deferred.promise; +} + +/** + * Checks that the placesContext menu is correctly attached to the + * controller of some view. Returns a Promise that resolves as soon + * as the context menu is closed. + * + * @param aItemWithContextMenu the item that we need to synthesize hte + * right click on in order to open the context menu. + */ +function checkPlacesContextMenu(aItemWithContextMenu) { + return Task.spawn(function* () { + let contextMenu = document.getElementById("placesContext"); + let newBookmarkItem = document.getElementById("placesContext_new:bookmark"); + let shownPromise = popupShown(contextMenu); + EventUtils.synthesizeMouseAtCenter(aItemWithContextMenu, + {type: "contextmenu", button: 2}); + yield shownPromise; + + ok(!newBookmarkItem.hasAttribute("disabled"), + "New bookmark item shouldn't be disabled"); + + yield closePopup(contextMenu); + }); +} + +/** + * Opens the bookmarks menu panel, and then opens each of the "special" + * submenus in that list. Then it checks that those submenu's context menus + * are properly hooked up to a controller. + */ +function checkSpecialContextMenus() { + return Task.spawn(function* () { + let contextMenu = document.getElementById("placesContext"); + let bookmarksMenuButton = document.getElementById(kBookmarksButton); + let bookmarksMenuPopup = document.getElementById("BMB_bookmarksPopup"); + + const kSpecialItemIDs = { + "BMB_bookmarksToolbar": "BMB_bookmarksToolbarPopup", + "BMB_unsortedBookmarks": "BMB_unsortedBookmarksPopup", + }; + + // Open the bookmarks menu button context menus and ensure that + // they have the proper views attached. + let shownPromise = bookmarksMenuPanelShown(); + let dropmarker = document.getAnonymousElementByAttribute(bookmarksMenuButton, + "anonid", "dropmarker"); + EventUtils.synthesizeMouseAtCenter(dropmarker, {}); + info("Waiting for bookmarks menu popup to show after clicking dropmarker.") + yield shownPromise; + + for (let menuID in kSpecialItemIDs) { + let menuItem = document.getElementById(menuID); + let menuPopup = document.getElementById(kSpecialItemIDs[menuID]); + let shownPromise = popupShown(menuPopup); + EventUtils.synthesizeMouseAtCenter(menuItem, {}); + yield shownPromise; + + yield checkPlacesContextMenu(menuPopup); + yield closePopup(menuPopup); + } + + yield closePopup(bookmarksMenuPopup); + }); +} + +/** + * Closes a focused popup by simulating pressing the Escape key, + * and returns a Promise that resolves as soon as the popup is closed. + * + * @param aPopup the popup node to close. + */ +function closePopup(aPopup) { + let hiddenPromise = popupHidden(aPopup); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + return hiddenPromise; +} + +/** + * Helper function that checks that the context menu of the + * bookmark toolbar items chevron popup is correctly hooked up + * to the controller of a view. + */ +function checkBookmarksItemsChevronContextMenu() { + return Task.spawn(function*() { + let chevronPopup = document.getElementById("PlacesChevronPopup"); + let shownPromise = popupShown(chevronPopup); + let chevron = document.getElementById("PlacesChevron"); + EventUtils.synthesizeMouseAtCenter(chevron, {}); + yield shownPromise; + yield waitForCondition(() => { + for (let child of chevronPopup.children) { + if (child.style.visibility != "hidden") + return true; + } + }); + yield checkPlacesContextMenu(chevronPopup); + yield closePopup(chevronPopup); + }); +} + +/** + * Forces the window to a width that causes the nav-bar to overflow + * its contents. Returns a Promise that resolves as soon as the + * overflowable nav-bar is showing its chevron. + */ +function overflowEverything() { + window.resizeTo(kSmallWidth, window.outerHeight); + return waitForCondition(() => gNavBar.hasAttribute("overflowing")); +} + +/** + * Returns the window to its original size from the start of the test, + * and returns a Promise that resolves when the nav-bar is no longer + * overflowing. + */ +function stopOverflowing() { + window.resizeTo(kOriginalWindowWidth, window.outerHeight); + return waitForCondition(() => !gNavBar.hasAttribute("overflowing")); +} + +/** + * Checks that an item with ID aID is overflowing in the nav-bar. + * + * @param aID the ID of the node to check for overflowingness. + */ +function checkOverflowing(aID) { + ok(!gNavBar.querySelector("#" + aID), + "Item with ID " + aID + " should no longer be in the gNavBar"); + let item = gOverflowList.querySelector("#" + aID); + ok(item, "Item with ID " + aID + " should be overflowing"); + is(item.getAttribute("overflowedItem"), "true", + "Item with ID " + aID + " should have overflowedItem attribute"); +} + +/** + * Checks that an item with ID aID is not overflowing in the nav-bar. + * + * @param aID the ID of hte node to check for non-overflowingness. + */ +function checkNotOverflowing(aID) { + ok(!gOverflowList.querySelector("#" + aID), + "Item with ID " + aID + " should no longer be overflowing"); + let item = gNavBar.querySelector("#" + aID); + ok(item, "Item with ID " + aID + " should be in the nav bar"); + ok(!item.hasAttribute("overflowedItem"), + "Item with ID " + aID + " should not have overflowedItem attribute"); +} + +/** + * Test that overflowing the bookmarks menu button doesn't break the + * context menus for the Unsorted and Bookmarks Toolbar menu items. + */ +add_task(function* testOverflowingBookmarksButtonContextMenu() { + ok(!gNavBar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + + // Open the Unsorted and Bookmarks Toolbar context menus and ensure + // that they have views attached. + yield checkSpecialContextMenus(); + + yield overflowEverything(); + checkOverflowing(kBookmarksButton); + + yield stopOverflowing(); + checkNotOverflowing(kBookmarksButton); + + yield checkSpecialContextMenus(); +}); + +/** + * Test that the bookmarks toolbar items context menu still works if moved + * to the menu from the overflow panel, and then back to the toolbar. + */ +add_task(function* testOverflowingBookmarksItemsContextMenu() { + yield PanelUI.ensureReady(); + + let bookmarksToolbarItems = document.getElementById(kBookmarksItems); + gCustomizeMode.addToToolbar(bookmarksToolbarItems); + yield checkPlacesContextMenu(bookmarksToolbarItems); + + yield overflowEverything(); + checkOverflowing(kBookmarksItems) + + gCustomizeMode.addToPanel(bookmarksToolbarItems); + + yield stopOverflowing(); + + gCustomizeMode.addToToolbar(bookmarksToolbarItems); + yield checkPlacesContextMenu(bookmarksToolbarItems); +}); + +/** + * Test that overflowing the bookmarks toolbar items doesn't cause the + * context menu in the bookmarks toolbar items chevron to stop working. + */ +add_task(function* testOverflowingBookmarksItemsChevronContextMenu() { + // If it's not already there, let's move the bookmarks toolbar items to + // the nav-bar. + let bookmarksToolbarItems = document.getElementById(kBookmarksItems); + gCustomizeMode.addToToolbar(bookmarksToolbarItems); + + // We make the PlacesToolbarItems element be super tiny in order to force + // the bookmarks toolbar items into overflowing and making the chevron + // show itself. + let placesToolbarItems = document.getElementById("PlacesToolbarItems"); + let placesChevron = document.getElementById("PlacesChevron"); + placesToolbarItems.style.maxWidth = "10px"; + yield waitForCondition(() => !placesChevron.collapsed); + + yield checkBookmarksItemsChevronContextMenu(); + + yield overflowEverything(); + checkOverflowing(kBookmarksItems); + + yield stopOverflowing(); + checkNotOverflowing(kBookmarksItems); + + yield checkBookmarksItemsChevronContextMenu(); + + placesToolbarItems.style.removeProperty("max-width"); +}); + +add_task(function* asyncCleanup() { + window.resizeTo(kOriginalWindowWidth, window.outerHeight); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js index f85d97275185..b34680c334e0 100644 --- a/browser/components/customizableui/test/head.js +++ b/browser/components/customizableui/test/head.js @@ -428,36 +428,49 @@ function promiseTabHistoryNavigation(aDirection = -1, aConditionFn) { return deferred.promise; } -function contextMenuShown(aContextMenu) { - let deferred = Promise.defer(); - let win = aContextMenu.ownerDocument.defaultView; - let timeoutId = win.setTimeout(() => { - deferred.reject("Context menu (" + aContextMenu.id + ") did not show within 20 seconds."); - }, 20000); - function onPopupShown(e) { - aContextMenu.removeEventListener("popupshown", onPopupShown); - win.clearTimeout(timeoutId); - deferred.resolve(); - }; - aContextMenu.addEventListener("popupshown", onPopupShown); - return deferred.promise; +function popupShown(aPopup) { + return promisePopupEvent(aPopup, "shown"); } -function contextMenuHidden(aContextMenu) { - let deferred = Promise.defer(); - let win = aContextMenu.ownerDocument.defaultView; - let timeoutId = win.setTimeout(() => { - deferred.reject("Context menu (" + aContextMenu.id + ") did not hide within 20 seconds."); - }, 20000); - function onPopupHidden(e) { - win.clearTimeout(timeoutId); - aContextMenu.removeEventListener("popuphidden", onPopupHidden); - deferred.resolve(); - }; - aContextMenu.addEventListener("popuphidden", onPopupHidden); - return deferred.promise; +function popupHidden(aPopup) { + return promisePopupEvent(aPopup, "hidden"); } +/** + * Returns a Promise that resolves when aPopup fires an event of type + * aEventType. Times out and rejects after 20 seconds. + * + * @param aPopup the popup to monitor for events. + * @param aEventSuffix the _suffix_ for the popup event type to watch for. + * + * Example usage: + * let popupShownPromise = promisePopupEvent(somePopup, "shown"); + * // ... something that opens a popup + * yield popupShownPromise; + * + * let popupHiddenPromise = promisePopupEvent(somePopup, "hidden"); + * // ... something that hides a popup + * yield popupHiddenPromise; + */ +function promisePopupEvent(aPopup, aEventSuffix) { + let deferred = Promise.defer(); + let win = aPopup.ownerDocument.defaultView; + let eventType = "popup" + aEventSuffix; + + let timeoutId = win.setTimeout(() => { + deferred.reject("Context menu (" + aPopup.id + ") did not fire " + + eventType + " within 20 seconds."); + }, 20000); + + function onPopupEvent(e) { + win.clearTimeout(timeoutId); + aPopup.removeEventListener(eventType, onPopupEvent); + deferred.resolve(); + }; + + aPopup.addEventListener(eventType, onPopupEvent); + return deferred.promise; +} // This is a simpler version of the context menu check that // exists in contextmenu_common.js. diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index 65353e3279aa..9425820a9aa5 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -81,6 +81,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "RemotePrompt", + "resource:///modules/RemotePrompt.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", "resource:///modules/sessionstore/SessionStore.jsm"); @@ -495,8 +498,10 @@ BrowserGlue.prototype = { SessionStore.init(); BrowserUITelemetry.init(); - if (Services.appinfo.browserTabsRemote) + if (Services.appinfo.browserTabsRemote) { ContentClick.init(); + RemotePrompt.init(); + } Services.obs.notifyObservers(null, "browser-ui-startup-complete", ""); }, diff --git a/browser/components/places/content/browserPlacesViews.js b/browser/components/places/content/browserPlacesViews.js index aadb637be2e6..f8a79ace6c8e 100644 --- a/browser/components/places/content/browserPlacesViews.js +++ b/browser/components/places/content/browserPlacesViews.js @@ -960,6 +960,10 @@ PlacesToolbar.prototype = { this._removeEventListeners(window, ["resize", "unload"], false); this._removeEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false); + if (this._chevron._placesView) { + this._chevron._placesView.uninit(); + } + PlacesViewBase.prototype.uninit.apply(this, arguments); }, diff --git a/browser/devtools/layoutview/test/browser.ini b/browser/devtools/layoutview/test/browser.ini index de6f938b9b27..9fd5919cbc09 100644 --- a/browser/devtools/layoutview/test/browser.ini +++ b/browser/devtools/layoutview/test/browser.ini @@ -1,4 +1,10 @@ [DEFAULT] +support-files = + head.js [browser_layoutview.js] skip-if = true +[browser_editablemodel.js] +[browser_editablemodel_allproperties.js] +[browser_editablemodel_border.js] +[browser_editablemodel_stylerules.js] diff --git a/browser/devtools/layoutview/test/browser_editablemodel.js b/browser/devtools/layoutview/test/browser_editablemodel.js new file mode 100644 index 000000000000..472c807ba6e4 --- /dev/null +++ b/browser/devtools/layoutview/test/browser_editablemodel.js @@ -0,0 +1,145 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function getStyle(node, property) { + return node.style.getPropertyValue(property); +} + +let doc; +let inspector; + +let test = asyncTest(function*() { + let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }"; + let html = "
" + + let content = yield loadTab("data:text/html," + encodeURIComponent(html)); + doc = content.document; + + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = yield gDevTools.showToolbox(target, "inspector"); + inspector = toolbox.getCurrentPanel(); + + inspector.sidebar.select("layoutview"); + yield inspector.sidebar.once("layoutview-ready"); + yield runTests(); + yield gDevTools.closeToolbox(toolbox); +}); + +addTest("Test that editing margin dynamically updates the document, pressing escape cancels the changes", +function*() { + let node = doc.getElementById("div1"); + is(getStyle(node, "margin-top"), "", "Should be no margin-top on the element.") + let view = yield selectNode(node); + + let span = view.document.querySelector(".margin.top > span"); + is(span.textContent, 5, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("3", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "margin-top"), "3px", "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "margin-top"), "", "Should be no margin-top on the element.") + is(span.textContent, 5, "Should have the right value in the box model."); +}); + +addTest("Test that arrow keys work correctly and pressing enter commits the changes", +function*() { + let node = doc.getElementById("div1"); + is(getStyle(node, "margin-left"), "", "Should be no margin-top on the element.") + let view = yield selectNode(node); + + let span = view.document.querySelector(".margin.left > span"); + is(span.textContent, 10, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "10px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_UP", {}, view); + yield waitForUpdate(); + + is(editor.value, "11px", "Should have the right value in the editor."); + is(getStyle(node, "margin-left"), "11px", "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_DOWN", {}, view); + yield waitForUpdate(); + + is(editor.value, "10px", "Should have the right value in the editor."); + is(getStyle(node, "margin-left"), "10px", "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_UP", { shiftKey: true }, view); + yield waitForUpdate(); + + is(editor.value, "20px", "Should have the right value in the editor."); + is(getStyle(node, "margin-left"), "20px", "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_RETURN", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.") + is(span.textContent, 20, "Should have the right value in the box model."); +}); + +addTest("Test that deleting the value removes the property but escape undoes that", +function*() { + let node = doc.getElementById("div1"); + is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.") + let view = yield selectNode(node); + + let span = view.document.querySelector(".margin.left > span"); + is(span.textContent, 20, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "20px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, view); + yield waitForUpdate(); + + is(editor.value, "", "Should have the right value in the editor."); + is(getStyle(node, "margin-left"), "", "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.") + is(span.textContent, 20, "Should have the right value in the box model."); +}); + +addTest("Test that deleting the value removes the property", +function*() { + let node = doc.getElementById("div1"); + node.style.marginRight = "15px"; + let view = yield selectNode(node); + + let span = view.document.querySelector(".margin.right > span"); + is(span.textContent, 15, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "15px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, view); + yield waitForUpdate(); + + is(editor.value, "", "Should have the right value in the editor."); + is(getStyle(node, "margin-right"), "", "Should have updated the margin."); + + EventUtils.synthesizeKey("VK_RETURN", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "margin-right"), "", "Should be the right margin-top on the element.") + is(span.textContent, 10, "Should have the right value in the box model."); +}); diff --git a/browser/devtools/layoutview/test/browser_editablemodel_allproperties.js b/browser/devtools/layoutview/test/browser_editablemodel_allproperties.js new file mode 100644 index 000000000000..82ea704cdcd5 --- /dev/null +++ b/browser/devtools/layoutview/test/browser_editablemodel_allproperties.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function getStyle(node, property) { + return node.style.getPropertyValue(property); +} + +let doc; +let inspector; + +let test = asyncTest(function*() { + let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }"; + let html = "
" + + let content = yield loadTab("data:text/html," + encodeURIComponent(html)); + doc = content.document; + + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = yield gDevTools.showToolbox(target, "inspector"); + inspector = toolbox.getCurrentPanel(); + + inspector.sidebar.select("layoutview"); + yield inspector.sidebar.once("layoutview-ready"); + yield runTests(); + yield gDevTools.closeToolbox(toolbox); +}); + +addTest("When all properties are set on the node editing one should work", +function*() { + let node = doc.getElementById("div1"); + node.style.padding = "5px"; + let view = yield selectNode(node); + + let span = view.document.querySelector(".padding.bottom > span"); + is(span.textContent, 5, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("7", {}, view); + yield waitForUpdate(); + + is(editor.value, "7", "Should have the right value in the editor."); + is(getStyle(node, "padding-bottom"), "7px", "Should have updated the padding"); + + EventUtils.synthesizeKey("VK_RETURN", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "padding-bottom"), "7px", "Should be the right padding.") + is(span.textContent, 7, "Should have the right value in the box model."); +}); + +addTest("When all properties are set on the node editing one should work", +function*() { + let node = doc.getElementById("div1"); + node.style.padding = "5px"; + let view = yield selectNode(node); + + let span = view.document.querySelector(".padding.left > span"); + is(span.textContent, 5, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("8", {}, view); + yield waitForUpdate(); + + is(editor.value, "8", "Should have the right value in the editor."); + is(getStyle(node, "padding-left"), "8px", "Should have updated the padding"); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "padding-left"), "5px", "Should be the right padding.") + is(span.textContent, 5, "Should have the right value in the box model."); +}); + +addTest("When all properties are set on the node deleting one should work", +function*() { + let node = doc.getElementById("div1"); + node.style.padding = "5px"; + let view = yield selectNode(node); + + let span = view.document.querySelector(".padding.left > span"); + is(span.textContent, 5, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, view); + yield waitForUpdate(); + + is(editor.value, "", "Should have the right value in the editor."); + is(getStyle(node, "padding-left"), "", "Should have updated the padding"); + + EventUtils.synthesizeKey("VK_RETURN", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "padding-left"), "", "Should be the right padding.") + is(span.textContent, 3, "Should have the right value in the box model."); +}); + +addTest("When all properties are set on the node deleting one then cancelling should work", +function*() { + let node = doc.getElementById("div1"); + node.style.padding = "5px"; + let view = yield selectNode(node); + + let span = view.document.querySelector(".padding.left > span"); + is(span.textContent, 5, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "5px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_DELETE", {}, view); + yield waitForUpdate(); + + is(editor.value, "", "Should have the right value in the editor."); + is(getStyle(node, "padding-left"), "", "Should have updated the padding"); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "padding-left"), "5px", "Should be the right padding.") + is(span.textContent, 5, "Should have the right value in the box model."); +}); diff --git a/browser/devtools/layoutview/test/browser_editablemodel_border.js b/browser/devtools/layoutview/test/browser_editablemodel_border.js new file mode 100644 index 000000000000..9f08fce2895a --- /dev/null +++ b/browser/devtools/layoutview/test/browser_editablemodel_border.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function getStyle(node, property) { + return node.style.getPropertyValue(property); +} + +let doc; +let inspector; + +let test = asyncTest(function*() { + let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }"; + let html = "
" + + let content = yield loadTab("data:text/html," + encodeURIComponent(html)); + doc = content.document; + + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = yield gDevTools.showToolbox(target, "inspector"); + inspector = toolbox.getCurrentPanel(); + + inspector.sidebar.select("layoutview"); + yield inspector.sidebar.once("layoutview-ready"); + yield runTests(); + yield gDevTools.closeToolbox(toolbox); +}); + +addTest("Test that adding a border applies a border style when necessary", +function*() { + let node = doc.getElementById("div1"); + is(getStyle(node, "border-top-width"), "", "Should have the right border"); + is(getStyle(node, "border-top-style"), "", "Should have the right border"); + let view = yield selectNode(node); + + let span = view.document.querySelector(".border.top > span"); + is(span.textContent, 0, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "0", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("1", {}, view); + yield waitForUpdate(); + + is(editor.value, "1", "Should have the right value in the editor."); + is(getStyle(node, "border-top-width"), "1px", "Should have the right border"); + is(getStyle(node, "border-top-style"), "solid", "Should have the right border"); + + EventUtils.synthesizeKey("VK_ESCAPE", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "border-top-width"), "", "Should be the right padding.") + is(getStyle(node, "border-top-style"), "", "Should have the right border"); + is(span.textContent, 0, "Should have the right value in the box model."); +}); diff --git a/browser/devtools/layoutview/test/browser_editablemodel_stylerules.js b/browser/devtools/layoutview/test/browser_editablemodel_stylerules.js new file mode 100644 index 000000000000..dad502213368 --- /dev/null +++ b/browser/devtools/layoutview/test/browser_editablemodel_stylerules.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function getStyle(node, property) { + return node.style.getPropertyValue(property); +} + +let doc; +let inspector; + +let test = asyncTest(function*() { + let style = "div { margin: 10px; padding: 3px } #div1 { margin-top: 5px } #div2 { border-bottom: 1em solid black; } #div3 { padding: 2em; }"; + let html = "
" + + let content = yield loadTab("data:text/html," + encodeURIComponent(html)); + doc = content.document; + + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = yield gDevTools.showToolbox(target, "inspector"); + inspector = toolbox.getCurrentPanel(); + + inspector.sidebar.select("layoutview"); + yield inspector.sidebar.once("layoutview-ready"); + yield runTests(); + yield gDevTools.closeToolbox(toolbox); +}); + +addTest("Test that entering units works", +function*() { + let node = doc.getElementById("div1"); + is(getStyle(node, "padding-top"), "", "Should have the right padding"); + let view = yield selectNode(node); + + let span = view.document.querySelector(".padding.top > span"); + is(span.textContent, 3, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "3px", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("1", {}, view); + yield waitForUpdate(); + EventUtils.synthesizeKey("e", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "padding-top"), "", "An invalid value is handled cleanly"); + + EventUtils.synthesizeKey("m", {}, view); + yield waitForUpdate(); + + is(editor.value, "1em", "Should have the right value in the editor."); + is(getStyle(node, "padding-top"), "1em", "Should have updated the padding."); + + EventUtils.synthesizeKey("VK_RETURN", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "padding-top"), "1em", "Should be the right padding.") + is(span.textContent, 16, "Should have the right value in the box model."); +}); + +addTest("Test that we pick up the value from a higher style rule", +function*() { + let node = doc.getElementById("div2"); + is(getStyle(node, "border-bottom-width"), "", "Should have the right border-bottom-width"); + let view = yield selectNode(node); + + let span = view.document.querySelector(".border.bottom > span"); + is(span.textContent, 16, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "1em", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("0", {}, view); + yield waitForUpdate(); + + is(editor.value, "0", "Should have the right value in the editor."); + is(getStyle(node, "border-bottom-width"), "0px", "Should have updated the border."); + + EventUtils.synthesizeKey("VK_RETURN", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "border-bottom-width"), "0px", "Should be the right border-bottom-width.") + is(span.textContent, 0, "Should have the right value in the box model."); +}); + +addTest("Test that shorthand properties are parsed correctly", +function*() { + let node = doc.getElementById("div3"); + is(getStyle(node, "padding-right"), "", "Should have the right padding"); + let view = yield selectNode(node); + + let span = view.document.querySelector(".padding.right > span"); + is(span.textContent, 32, "Should have the right value in the box model."); + + EventUtils.synthesizeMouseAtCenter(span, {}, view); + let editor = view.document.querySelector(".styleinspector-propertyeditor"); + ok(editor, "Should have opened the editor."); + is(editor.value, "2em", "Should have the right value in the editor."); + + EventUtils.synthesizeKey("VK_RETURN", {}, view); + yield waitForUpdate(); + + is(getStyle(node, "padding-right"), "", "Should be the right padding.") + is(span.textContent, 32, "Should have the right value in the box model."); +}); diff --git a/browser/devtools/layoutview/test/browser_layoutview.js b/browser/devtools/layoutview/test/browser_layoutview.js index 9c2d8511afe4..4e268c2bde87 100644 --- a/browser/devtools/layoutview/test/browser_layoutview.js +++ b/browser/devtools/layoutview/test/browser_layoutview.js @@ -1,134 +1,99 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); -let TargetFactory = devtools.TargetFactory; +// Expected values: +let res1 = [ + {selector: "#element-size", value: "160x160"}, + {selector: ".size > span", value: "100x100"}, + {selector: ".margin.top > span", value: 30}, + {selector: ".margin.left > span", value: "auto"}, + {selector: ".margin.bottom > span", value: 30}, + {selector: ".margin.right > span", value: "auto"}, + {selector: ".padding.top > span", value: 20}, + {selector: ".padding.left > span", value: 20}, + {selector: ".padding.bottom > span", value: 20}, + {selector: ".padding.right > span", value: 20}, + {selector: ".border.top > span", value: 10}, + {selector: ".border.left > span", value: 10}, + {selector: ".border.bottom > span", value: 10}, + {selector: ".border.right > span", value: 10}, +]; -function test() { - waitForExplicitFinish(); +let res2 = [ + {selector: "#element-size", value: "190x210"}, + {selector: ".size > span", value: "100x150"}, + {selector: ".margin.top > span", value: 30}, + {selector: ".margin.left > span", value: "auto"}, + {selector: ".margin.bottom > span", value: 30}, + {selector: ".margin.right > span", value: "auto"}, + {selector: ".padding.top > span", value: 20}, + {selector: ".padding.left > span", value: 20}, + {selector: ".padding.bottom > span", value: 20}, + {selector: ".padding.right > span", value: 50}, + {selector: ".border.top > span", value: 10}, + {selector: ".border.left > span", value: 10}, + {selector: ".border.bottom > span", value: 10}, + {selector: ".border.right > span", value: 10}, +]; - gDevTools.testing = true; - SimpleTest.registerCleanupFunction(() => { - gDevTools.testing = false; - }); - - Services.prefs.setBoolPref("devtools.inspector.sidebarOpen", true); - - let doc; - let node; - let view; - let inspector; - - // Expected values: - let res1 = [ - {selector: "#element-size", value: "160x160"}, - {selector: ".size > span", value: "100x100"}, - {selector: ".margin.top > span", value: 30}, - {selector: ".margin.left > span", value: "auto"}, - {selector: ".margin.bottom > span", value: 30}, - {selector: ".margin.right > span", value: "auto"}, - {selector: ".padding.top > span", value: 20}, - {selector: ".padding.left > span", value: 20}, - {selector: ".padding.bottom > span", value: 20}, - {selector: ".padding.right > span", value: 20}, - {selector: ".border.top > span", value: 10}, - {selector: ".border.left > span", value: 10}, - {selector: ".border.bottom > span", value: 10}, - {selector: ".border.right > span", value: 10}, - ]; - - let res2 = [ - {selector: "#element-size", value: "190x210"}, - {selector: ".size > span", value: "100x150"}, - {selector: ".margin.top > span", value: 30}, - {selector: ".margin.left > span", value: "auto"}, - {selector: ".margin.bottom > span", value: 30}, - {selector: ".margin.right > span", value: "auto"}, - {selector: ".padding.top > span", value: 20}, - {selector: ".padding.left > span", value: 20}, - {selector: ".padding.bottom > span", value: 20}, - {selector: ".padding.right > span", value: 50}, - {selector: ".border.top > span", value: 10}, - {selector: ".border.left > span", value: 10}, - {selector: ".border.bottom > span", value: 10}, - {selector: ".border.right > span", value: 10}, - ]; - - gBrowser.selectedTab = gBrowser.addTab(); - gBrowser.selectedBrowser.addEventListener("load", function onload() { - gBrowser.selectedBrowser.removeEventListener("load", onload, true); - doc = content.document; - waitForFocus(setupTest, content); - }, true); +let inspector; +let view; +let test = asyncTest(function*() { let style = "div { position: absolute; top: 42px; left: 42px; height: 100px; width: 100px; border: 10px solid black; padding: 20px; margin: 30px auto;}"; let html = "
" - content.location = "data:text/html," + encodeURIComponent(html); - function setupTest() { - node = doc.querySelector("div"); - ok(node, "node found"); + let content = yield loadTab("data:text/html," + encodeURIComponent(html)); + let node = content.document.querySelector("div"); + ok(node, "node found"); - let target = TargetFactory.forTab(gBrowser.selectedTab); - gDevTools.showToolbox(target, "inspector").then(function(toolbox) { - openLayoutView(toolbox.getCurrentPanel()); - }); + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = yield gDevTools.showToolbox(target, "inspector"); + inspector = toolbox.getCurrentPanel(); + + info("Inspector open"); + + inspector.sidebar.select("layoutview"); + yield inspector.sidebar.once("layoutview-ready"); + + inspector.selection.setNode(node); + yield inspector.once("inspector-updated"); + + info("Layout view ready"); + + view = inspector.sidebar.getWindowForTab("layoutview"); + ok(!!view.layoutview, "LayoutView document is alive."); + + yield runTests(); + + executeSoon(function() { + inspector._toolbox.destroy(); + }); + + yield gDevTools.once("toolbox-destroyed"); +}); + +addTest("Test that the initial values of the box model are correct", +function*() { + let viewdoc = view.document; + + for (let i = 0; i < res1.length; i++) { + let elt = viewdoc.querySelector(res1[i].selector); + is(elt.textContent, res1[i].value, res1[i].selector + " has the right value."); } +}); - function openLayoutView(aInspector) { - inspector = aInspector; +addTest("Test that changing the document updates the box model", +function*() { + let viewdoc = view.document; - info("Inspector open"); + inspector.selection.node.style.height = "150px"; + inspector.selection.node.style.paddingRight = "50px"; - inspector.sidebar.select("layoutview"); - inspector.sidebar.once("layoutview-ready", () => { - inspector.selection.setNode(node); - inspector.once("inspector-updated", viewReady); - }); + yield waitForUpdate(); + + for (let i = 0; i < res2.length; i++) { + let elt = viewdoc.querySelector(res2[i].selector); + is(elt.textContent, res2[i].value, res2[i].selector + " has the right value after style update."); } - - - function viewReady() { - info("Layout view ready"); - - view = inspector.sidebar.getWindowForTab("layoutview"); - - ok(!!view.layoutview, "LayoutView document is alive."); - - test1(); - } - - function test1() { - let viewdoc = view.document; - - for (let i = 0; i < res1.length; i++) { - let elt = viewdoc.querySelector(res1[i].selector); - is(elt.textContent, res1[i].value, res1[i].selector + " has the right value."); - } - - inspector.once("layoutview-updated", test2); - - inspector.selection.node.style.height = "150px"; - inspector.selection.node.style.paddingRight = "50px"; - } - - function test2() { - let viewdoc = view.document; - - for (let i = 0; i < res2.length; i++) { - let elt = viewdoc.querySelector(res2[i].selector); - is(elt.textContent, res2[i].value, res2[i].selector + " has the right value after style update."); - } - - executeSoon(function() { - gDevTools.once("toolbox-destroyed", finishUp); - inspector._toolbox.destroy(); - }); - } - - function finishUp() { - Services.prefs.clearUserPref("devtools.inspector.sidebarOpen"); - gBrowser.removeCurrentTab(); - finish(); - } -} +}); diff --git a/browser/devtools/layoutview/test/head.js b/browser/devtools/layoutview/test/head.js new file mode 100644 index 000000000000..618b5e66780f --- /dev/null +++ b/browser/devtools/layoutview/test/head.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/Task.jsm"); + +let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +const promise = devtools.require("sdk/core/promise"); +let TargetFactory = devtools.TargetFactory; + +Services.prefs.setBoolPref("devtools.inspector.sidebarOpen", true); +Services.prefs.setIntPref("devtools.toolbox.footer.height", 350); +gDevTools.testing = true; +SimpleTest.registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.inspector.sidebarOpen"); + Services.prefs.clearUserPref("devtools.toolbox.footer.height"); + gDevTools.testing = false; +}); + +// All tests are async in general +waitForExplicitFinish(); + +function loadTab(url) { + let deferred = promise.defer(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + waitForFocus(function() { + deferred.resolve(content); + }, content); + }, true); + + content.location = url; + + return deferred.promise; +} + +function selectNode(aNode) { + info("selecting node"); + let onSelect = inspector.once("layoutview-updated"); + inspector.selection.setNode(aNode, "test"); + return onSelect.then(() => { + let view = inspector.sidebar.getWindowForTab("layoutview"); + ok(!!view.layoutview, "LayoutView document is alive."); + + return view; + }); +} + +function waitForUpdate() { + return inspector.once("layoutview-updated"); +} + +function asyncTest(testfunc) { + return Task.async(function*() { + let initialTab = gBrowser.selectedTab; + + yield testfunc(); + + // Remove all tabs except for the initial tab. This is basically + // gBrowser.removeAllTabsBut without the animation + let tabs = gBrowser.visibleTabs; + gBrowser.selectedTab = initialTab; + for (let i = tabs.length - 1; i >= 0; i--) { + if (tabs[i] != initialTab) + gBrowser.removeTab(tabs[i]); + } + + // Reset the sidebar back to the default + Services.prefs.setCharPref("devtools.inspector.activeSidebar", "ruleview"); + + finish(); + }); +} + +var TESTS = []; + +function addTest(message, func) { + TESTS.push([message, Task.async(func)]) +} + +var runTests = Task.async(function*() { + for (let [message, test] of TESTS) { + info(message); + yield test(); + } +}); diff --git a/browser/devtools/layoutview/view.css b/browser/devtools/layoutview/view.css index 232102d84a75..226ef1768788 100644 --- a/browser/devtools/layoutview/view.css +++ b/browser/devtools/layoutview/view.css @@ -86,9 +86,16 @@ body { #main > p > span { vertical-align: middle; pointer-events: auto; +} + +.size > span { cursor: default; } +.editable { + -moz-user-select: text; +} + .top, .bottom { width: calc(100% - 2px); @@ -179,4 +186,3 @@ body.dim > #main > p, body.dim > #main > .tooltip { visibility: hidden; } - diff --git a/browser/devtools/layoutview/view.js b/browser/devtools/layoutview/view.js index 1f9b341baef7..13f22c01cb5a 100644 --- a/browser/devtools/layoutview/view.js +++ b/browser/devtools/layoutview/view.js @@ -11,10 +11,113 @@ const Ci = Components.interfaces; const Cc = Components.classes; Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/devtools/Loader.jsm"); Cu.import("resource://gre/modules/devtools/Console.jsm"); const promise = devtools.require("sdk/core/promise"); +const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor"); +const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils"); + +const NUMERIC = /^-?[\d\.]+$/; + +/** + * An instance of EditingSession tracks changes that have been made during the + * modification of box model values. All of these changes can be reverted by + * calling revert. + * + * @param doc A DOM document that can be used to test style rules. + * @param rules An array of the style rules defined for the node being edited. + * These should be in order of priority, least important first. + */ +function EditingSession(doc, rules) { + this._doc = doc; + this._rules = rules; + this._modifications = new Map(); +} + +EditingSession.prototype = { + /** + * Gets the value of a single property from the CSS rule. + * + * @param rule The CSS rule + * @param property The name of the property + */ + getPropertyFromRule: function(rule, property) { + let dummyStyle = this._element.style; + + dummyStyle.cssText = rule.cssText; + return dummyStyle.getPropertyValue(property); + }, + + /** + * Returns the current value for a property as a string or the empty string if + * no style rules affect the property. + * + * @param property The name of the property as a string + */ + getProperty: function(property) { + // Create a hidden element for getPropertyFromRule to use + let div = this._doc.createElement("div"); + div.setAttribute("style", "display: none"); + this._doc.body.appendChild(div); + this._element = this._doc.createElement("p"); + div.appendChild(this._element); + + // As the rules are in order of priority we can just iterate until we find + // the first that defines a value for the property and return that. + for (let rule of this._rules) { + let value = this.getPropertyFromRule(rule, property); + if (value !== "") { + div.remove(); + return value; + } + } + div.remove(); + return ""; + }, + + /** + * Sets a number of properties on the node. Returns a promise that will be + * resolved when the modifications are complete. + * + * @param properties An array of properties, each is an object with name and + * value properties. If the value is "" then the property + * is removed. + */ + setProperties: function(properties) { + let modifications = this._rules[0].startModifyingProperties(); + + for (let property of properties) { + if (!this._modifications.has(property.name)) + this._modifications.set(property.name, this.getPropertyFromRule(this._rules[0], property.name)); + + if (property.value == "") + modifications.removeProperty(property.name); + else + modifications.setProperty(property.name, property.value, ""); + } + + return modifications.apply().then(null, console.error); + }, + + /** + * Reverts all of the property changes made by this instance. Returns a + * promise that will be resolved when complete. + */ + revert: function() { + let modifications = this._rules[0].startModifyingProperties(); + + for (let [property, value] of this._modifications) { + if (value != "") + modifications.setProperty(property, value, ""); + else + modifications.removeProperty(property); + } + + return modifications.apply().then(null, console.error); + } +}; function LayoutView(aInspector, aWindow) { @@ -54,11 +157,17 @@ LayoutView.prototype = { marginBottom: {selector: ".margin.bottom > span", property: "margin-bottom", value: undefined}, + // margin-left is a shorthand for some internal properties, + // margin-left-ltr-source and margin-left-rtl-source for example. The + // real margin value we want is in margin-left-value marginLeft: {selector: ".margin.left > span", property: "margin-left", + realProperty: "margin-left-value", value: undefined}, + // margin-right behaves the same as margin-left marginRight: {selector: ".margin.right > span", property: "margin-right", + realProperty: "margin-right-value", value: undefined}, paddingTop: {selector: ".padding.top > span", property: "padding-top", @@ -66,11 +175,15 @@ LayoutView.prototype = { paddingBottom: {selector: ".padding.bottom > span", property: "padding-bottom", value: undefined}, + // padding-left behaves the same as margin-left paddingLeft: {selector: ".padding.left > span", property: "padding-left", + realProperty: "padding-left-value", value: undefined}, + // padding-right behaves the same as margin-left paddingRight: {selector: ".padding.right > span", property: "padding-right", + realProperty: "padding-right-value", value: undefined}, borderTop: {selector: ".border.top > span", property: "border-top-width", @@ -86,9 +199,58 @@ LayoutView.prototype = { value: undefined}, }; + // Make each element the dimensions editable + for (let i in this.map) { + if (i == "position") + continue; + + let dimension = this.map[i]; + editableItem({ element: this.doc.querySelector(dimension.selector) }, (element, event) => { + this.initEditor(element, event, dimension); + }); + } + this.onNewNode(); }, + /** + * Called when the user clicks on one of the editable values in the layoutview + */ + initEditor: function LV_initEditor(element, event, dimension) { + let { property, realProperty } = dimension; + if (!realProperty) + realProperty = property; + let session = new EditingSession(document, this.elementRules); + let initialValue = session.getProperty(realProperty); + + new InplaceEditor({ + element: element, + initial: initialValue, + + change: (value) => { + if (NUMERIC.test(value)) + value += "px"; + let properties = [ + { name: property, value: value } + ] + + if (property.substring(0, 7) == "border-") { + let bprop = property.substring(0, property.length - 5) + "style"; + let style = session.getProperty(bprop); + if (!style || style == "none" || style == "hidden") + properties.push({ name: bprop, value: "solid" }); + } + + session.setProperties(properties); + }, + + done: (value, commit) => { + if (!commit) + session.revert(); + } + }, event); + }, + /** * Is the layoutview visible in the sidebar? */ @@ -158,23 +320,28 @@ LayoutView.prototype = { /** * Compute the dimensions of the node and update the values in - * the layoutview/view.xhtml document. + * the layoutview/view.xhtml document. Returns a promise that will be resolved + * when complete. */ update: function LV_update() { - if (!this.isActive() || - !this.inspector.selection.isConnected() || - !this.inspector.selection.isElementNode()) { - return promise.resolve(undefined); - } + let lastRequest = Task.spawn((function*() { + if (!this.isActive() || + !this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode()) { + return; + } + + let node = this.inspector.selection.nodeFront; + let layout = yield this.inspector.pageStyle.getLayout(node, { + autoMargins: !this.dimmed + }); + let styleEntries = yield this.inspector.pageStyle.getApplied(node, {}); - let node = this.inspector.selection.nodeFront; - let lastRequest = this.inspector.pageStyle.getLayout(node, { - autoMargins: !this.dimmed - }).then(layout => { // If a subsequent request has been made, wait for that one instead. if (this._lastRequest != lastRequest) { return this._lastRequest; } + this._lastRequest = null; let width = layout.width; let height = layout.height; @@ -233,12 +400,12 @@ LayoutView.prototype = { this.sizeLabel.textContent = newValue; } - this.inspector.emit("layoutview-updated"); - return null; - }); + this.elementRules = [e.rule for (e of styleEntries)]; - this._lastRequest = lastRequest; - return this._lastRequest; + this.inspector.emit("layoutview-updated"); + }).bind(this)).then(null, console.error); + + return this._lastRequest = lastRequest; }, showBoxModel: function(options={}) { diff --git a/browser/devtools/layoutview/view.xhtml b/browser/devtools/layoutview/view.xhtml index 05e76626b366..261d1a42a9e2 100644 --- a/browser/devtools/layoutview/view.xhtml +++ b/browser/devtools/layoutview/view.xhtml @@ -39,20 +39,20 @@ -

-

-

-

+

+

+

+

-

-

-

-

+

+

+

+

-

-

-

-

+

+

+

+

@@ -60,5 +60,8 @@ +
+

+
diff --git a/browser/devtools/netmonitor/test/head.js b/browser/devtools/netmonitor/test/head.js index 376f02a7483b..859716d1a084 100644 --- a/browser/devtools/netmonitor/test/head.js +++ b/browser/devtools/netmonitor/test/head.js @@ -211,7 +211,8 @@ function waitForNetworkEvents(aMonitor, aGetRequests, aPostRequests = 0) { function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) { info("> Verifying: " + aMethod + " " + aUrl + " " + aData.toSource()); - info("> Request: " + aRequestItem.attachment.toSource()); + // This bloats log sizes significantly in automation (bug 992485) + //info("> Request: " + aRequestItem.attachment.toSource()); let requestsMenu = aRequestItem.ownerView; let widgetIndex = requestsMenu.indexOfItem(aRequestItem); diff --git a/browser/devtools/shared/inplace-editor.js b/browser/devtools/shared/inplace-editor.js index 3058304b038b..de5c79bfbfd3 100644 --- a/browser/devtools/shared/inplace-editor.js +++ b/browser/devtools/shared/inplace-editor.js @@ -376,6 +376,11 @@ InplaceEditor.prototype = { this.input.setSelectionRange(newValue.start, newValue.end); this._doValidation(); + // Call the user's change handler if available. + if (this.change) { + this.change(this.input.value.trim()); + } + return true; }, diff --git a/browser/experiments/test/xpcshell/test_api.js b/browser/experiments/test/xpcshell/test_api.js index 05fd7dcbe296..d92083fad851 100644 --- a/browser/experiments/test/xpcshell/test_api.js +++ b/browser/experiments/test/xpcshell/test_api.js @@ -73,6 +73,9 @@ add_task(function* test_setup() { }); }); +add_task(function* test_contract() { + Cc["@mozilla.org/browser/experiments-service;1"].getService(); +}); // Test basic starting and stopping of experiments. diff --git a/browser/fuel/src/fuelApplication.js b/browser/fuel/src/fuelApplication.js index 225b1b52993c..89d568ae1336 100644 --- a/browser/fuel/src/fuelApplication.js +++ b/browser/fuel/src/fuelApplication.js @@ -729,6 +729,8 @@ var ApplicationFactory = { }; +#include ../../../toolkit/components/exthelper/extApplication.js + //================================================= // Application constructor function Application() { @@ -737,60 +739,80 @@ function Application() { //================================================= // Application implementation -Application.prototype = { +function ApplicationPrototype() { // for nsIClassInfo + XPCOMUtils - classID: APPLICATION_CID, + this.classID = APPLICATION_CID; // redefine the default factory for XPCOMUtils - _xpcom_factory: ApplicationFactory, + this._xpcom_factory = ApplicationFactory; // for nsISupports - QueryInterface: XPCOMUtils.generateQI([Ci.fuelIApplication, Ci.extIApplication, - Ci.nsIObserver, Ci.nsISupportsWeakReference]), + this.QueryInterface = XPCOMUtils.generateQI([ + Ci.fuelIApplication, + Ci.extIApplication, + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]); // for nsIClassInfo - classInfo: XPCOMUtils.generateCI({classID: APPLICATION_CID, - contractID: APPLICATION_CONTRACTID, - interfaces: [Ci.fuelIApplication, - Ci.extIApplication, - Ci.nsIObserver], - flags: Ci.nsIClassInfo.SINGLETON}), + this.classInfo = XPCOMUtils.generateCI({ + classID: APPLICATION_CID, + contractID: APPLICATION_CONTRACTID, + interfaces: [ + Ci.fuelIApplication, + Ci.extIApplication, + Ci.nsIObserver + ], + flags: Ci.nsIClassInfo.SINGLETON + }); // for nsIObserver - observe: function app_observe(aSubject, aTopic, aData) { + this.observe = function (aSubject, aTopic, aData) { // Call the extApplication version of this function first - this.__proto__.__proto__.observe.call(this, aSubject, aTopic, aData); + var superPrototype = Object.getPrototypeOf(Object.getPrototypeOf(this)); + superPrototype.observe.call(this, aSubject, aTopic, aData); if (aTopic == "xpcom-shutdown") { this._obs.removeObserver(this, "xpcom-shutdown"); Utilities.free(); } - }, + }; - get bookmarks() { - let bookmarks = new BookmarkRoots(); - this.__defineGetter__("bookmarks", function() bookmarks); - return this.bookmarks; - }, + Object.defineProperty(this, "bookmarks", { + get: function bookmarks () { + let bookmarks = new BookmarkRoots(); + Object.defineProperty(this, "bookmarks", { value: bookmarks }); + return this.bookmarks; + }, + enumerable: true, + configurable: true + }); - get windows() { - var win = []; - var browserEnum = Utilities.windowMediator.getEnumerator("navigator:browser"); + Object.defineProperty(this, "windows", { + get: function windows() { + var win = []; + var browserEnum = Utilities.windowMediator.getEnumerator("navigator:browser"); - while (browserEnum.hasMoreElements()) - win.push(getWindow(browserEnum.getNext())); + while (browserEnum.hasMoreElements()) + win.push(getWindow(browserEnum.getNext())); - return win; - }, + return win; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(this, "activeWindow", { + get: () => getWindow(Utilities.windowMediator.getMostRecentWindow("navigator:browser")), + enumerable: true, + configurable: true + }); - get activeWindow() { - return getWindow(Utilities.windowMediator.getMostRecentWindow("navigator:browser")); - } }; -#include ../../../toolkit/components/exthelper/extApplication.js - // set the proto, defined in extApplication.js -Application.prototype.__proto__ = extApplication.prototype; +ApplicationPrototype.prototype = extApplication.prototype; + +Application.prototype = new ApplicationPrototype(); this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Application]); diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index b5e1a02206d9..a1ae2ac58b6f 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -368,6 +368,8 @@ @BINPATH@/browser/components/BrowserPlaces.manifest @BINPATH@/browser/components/devtools-clhandler.manifest @BINPATH@/browser/components/devtools-clhandler.js +@BINPATH@/browser/components/Experiments.manifest +@BINPATH@/browser/components/ExperimentsService.js @BINPATH@/components/Downloads.manifest @BINPATH@/components/DownloadLegacy.js @BINPATH@/components/BrowserPageThumbs.manifest diff --git a/browser/installer/windows/nsis/stub.nsi b/browser/installer/windows/nsis/stub.nsi index 8a8b12698bc3..86a7cf3fc375 100644 --- a/browser/installer/windows/nsis/stub.nsi +++ b/browser/installer/windows/nsis/stub.nsi @@ -707,7 +707,7 @@ Function SendPing Call RelativeGotoPage !else ${NSD_CreateTimer} OnPing ${DownloadIntervalMS} - InetBgDL::Get "${BaseURLStubPing}/${StubURLVersion}/${Channel}/${UpdateChannel}/${AB_CD}/$R0/$R1/$5/$6/$7/$8/$9/$ExitCode/$FirefoxLaunchCode/$DownloadRetryCount/$DownloadedBytes/$DownloadSizeBytes/$IntroPhaseSeconds/$OptionsPhaseSeconds/$0/$1/$DownloadFirstTransferSeconds/$2/$3/$4/$InitialInstallRequirementsCode/$OpenedDownloadPage/$ExistingProfile/$ExistingVersion/$ExistingBuildID/$R5/$R6/$R7/$R8/$R2/$R3/$DownloadServerIP" \ + InetBgDL::Get "${BaseURLStubPing}/${StubURLVersion}${StubURLVersionAppend}/${Channel}/${UpdateChannel}/${AB_CD}/$R0/$R1/$5/$6/$7/$8/$9/$ExitCode/$FirefoxLaunchCode/$DownloadRetryCount/$DownloadedBytes/$DownloadSizeBytes/$IntroPhaseSeconds/$OptionsPhaseSeconds/$0/$1/$DownloadFirstTransferSeconds/$2/$3/$4/$InitialInstallRequirementsCode/$OpenedDownloadPage/$ExistingProfile/$ExistingVersion/$ExistingBuildID/$R5/$R6/$R7/$R8/$R2/$R3/$DownloadServerIP" \ "$PLUGINSDIR\_temp" /END !endif ${Else} diff --git a/browser/installer/windows/nsis/uninstaller.nsi b/browser/installer/windows/nsis/uninstaller.nsi index 4c0544a2b5b8..a9ba76bfeb9f 100755 --- a/browser/installer/windows/nsis/uninstaller.nsi +++ b/browser/installer/windows/nsis/uninstaller.nsi @@ -99,7 +99,6 @@ VIAddVersionKey "OriginalFilename" "helper.exe" !insertmacro un.CheckForFilesInUse !insertmacro un.CleanUpdateDirectories !insertmacro un.CleanVirtualStore -!insertmacro un.DeleteRelativeProfiles !insertmacro un.DeleteShortcuts !insertmacro un.GetLongPath !insertmacro un.GetSecondInstallPath @@ -256,15 +255,6 @@ Section "Uninstall" ClearErrors ${EndIf} - ${MUI_INSTALLOPTIONS_READ} $0 "unconfirm.ini" "Field 3" "State" - ${If} "$0" == "1" - ${un.DeleteRelativeProfiles} "Mozilla\Firefox" - ${un.DeleteRelativeProfiles} "Mozilla\MetroFirefox" - RmDir "$APPDATA\Mozilla\Extensions\{ec8030f7-c20a-464f-9b0e-13a3a9e97384}" - RmDir "$APPDATA\Mozilla\Extensions" - RmDir "$APPDATA\Mozilla" - ${EndIf} - ; setup the application model id registration value ${un.InitHashAppModelId} "$INSTDIR" "Software\Mozilla\${AppName}\TaskBarIDs" @@ -551,7 +541,7 @@ Function un.preConfirm ${EndIf} ; Setup the unconfirm.ini file for the Custom Uninstall Confirm Page - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Settings" NumFields "5" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Settings" NumFields "3" WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 1" Type "label" WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 1" Text "$(UN_CONFIRM_UNINSTALLED_FROM)" @@ -571,44 +561,22 @@ Function un.preConfirm WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 2" Bottom "30" WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 2" flags "READONLY" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Type "checkbox" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Text "$(UN_REMOVE_PROFILES)" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Type "label" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Text "$(UN_CONFIRM_CLICK)" WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Left "0" WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Right "-1" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Top "40" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Bottom "50" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" State "0" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" flags "NOTIFY" - - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Type "text" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" State "$(UN_REMOVE_PROFILES_DESC)" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Left "0" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Right "-1" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Top "52" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Bottom "120" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" flags "MULTILINE|READONLY" - - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 5" Type "label" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 5" Text "$(UN_CONFIRM_CLICK)" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 5" Left "0" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 5" Right "-1" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 5" Top "130" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 5" Bottom "150" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Top "130" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Bottom "150" ${If} "$TmpVal" == "true" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 6" Type "label" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 6" Text "$(SUMMARY_REBOOT_REQUIRED_UNINSTALL)" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 6" Left "0" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 6" Right "-1" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 6" Top "35" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 6" Bottom "45" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Type "label" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Text "$(SUMMARY_REBOOT_REQUIRED_UNINSTALL)" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Left "0" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Right "-1" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Top "35" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Bottom "45" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Settings" NumFields "6" - - ; To insert this control reset Top / Bottom for controls below this one - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Top "55" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 3" Bottom "65" - WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Field 4" Top "67" + WriteINIStr "$PLUGINSDIR\unconfirm.ini" "Settings" NumFields "4" ${EndIf} !insertmacro MUI_HEADER_TEXT "$(UN_CONFIRM_PAGE_TITLE)" "$(UN_CONFIRM_PAGE_SUBTITLE)" @@ -616,9 +584,6 @@ Function un.preConfirm ; focus. This sets the focus to the Install button instead. !insertmacro MUI_INSTALLOPTIONS_INITDIALOG "unconfirm.ini" GetDlgItem $0 $HWNDPARENT 1 - ${MUI_INSTALLOPTIONS_READ} $1 "unconfirm.ini" "Field 4" "HWND" - SetCtlColors $1 0x000000 0xFFFFEE - ShowWindow $1 ${SW_HIDE} System::Call "user32::SetFocus(i r0, i 0x0007, i,i)i" ${MUI_INSTALLOPTIONS_READ} $1 "unconfirm.ini" "Field 2" "HWND" SendMessage $1 ${WM_SETTEXT} 0 "STR:$INSTDIR" @@ -626,19 +591,6 @@ Function un.preConfirm FunctionEnd Function un.leaveConfirm - ${MUI_INSTALLOPTIONS_READ} $0 "unconfirm.ini" "Settings" "State" - StrCmp $0 "3" +1 continue - ${MUI_INSTALLOPTIONS_READ} $0 "unconfirm.ini" "Field 3" "State" - ${MUI_INSTALLOPTIONS_READ} $1 "unconfirm.ini" "Field 4" "HWND" - StrCmp $0 1 +1 +3 - ShowWindow $1 ${SW_SHOW} - Abort - - ShowWindow $1 ${SW_HIDE} - Abort - - continue: - ; Try to delete the app executable and if we can't delete it try to find the ; app's message window and prompt the user to close the app. This allows ; running an instance that is located in another directory. If for whatever diff --git a/browser/locales/en-US/installer/custom.properties b/browser/locales/en-US/installer/custom.properties index 770c8543f48c..d30aedf6fc0e 100644 --- a/browser/locales/en-US/installer/custom.properties +++ b/browser/locales/en-US/installer/custom.properties @@ -58,8 +58,6 @@ UN_CONFIRM_PAGE_TITLE=Uninstall $BrandFullName UN_CONFIRM_PAGE_SUBTITLE=Remove $BrandFullName from your computer. UN_CONFIRM_UNINSTALLED_FROM=$BrandShortName will be uninstalled from the following location: UN_CONFIRM_CLICK=Click Uninstall to continue. -UN_REMOVE_PROFILES=&Remove my $BrandShortName personal data and customizations -UN_REMOVE_PROFILES_DESC=This will permanently remove your bookmarks, saved passwords, cookies and customizations. You may wish to keep this information if you plan on installing another version of $BrandShortName in the future. BANNER_CHECK_EXISTING=Checking existing installation… diff --git a/browser/modules/RemotePrompt.jsm b/browser/modules/RemotePrompt.jsm new file mode 100644 index 000000000000..4c5866a6cb6f --- /dev/null +++ b/browser/modules/RemotePrompt.jsm @@ -0,0 +1,95 @@ +/* vim: set ts=2 sw=2 et tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; + +this.EXPORTED_SYMBOLS = [ "RemotePrompt" ]; + +Cu.import("resource:///modules/PlacesUIUtils.jsm"); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/SharedPromptUtils.jsm"); + +let RemotePrompt = { + init: function() { + let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager); + mm.addMessageListener("Prompt:Open", this); + }, + + receiveMessage: function(message) { + switch (message.name) { + case "Prompt:Open": + if (message.data.uri) { + this.openModalWindow(message.data, message.target); + } else { + this.openTabPrompt(message.data, message.target) + } + break; + } + }, + + openTabPrompt: function(args, browser) { + let window = browser.ownerDocument.defaultView; + let tabPrompt = window.gBrowser.getTabModalPromptBox(browser) + let callbackInvoked = false; + let newPrompt; + let promptId = args._remoteId; + + function onPromptClose(forceCleanup) { + if (newPrompt) + tabPrompt.removePrompt(newPrompt); + + PromptUtils.fireDialogEvent(window, "DOMModalDialogClosed", browser); + browser.messageManager.sendAsyncMessage("Prompt:Close", args); + } + + browser.messageManager.addMessageListener("Prompt:ForceClose", function listener(message) { + // If this was for another prompt in the same tab, ignore it. + if (message.data._remoteId !== promptId) { + return; + } + + browser.messageManager.removeMessageListener("Prompt:ForceClose", listener); + + if (newPrompt) { + newPrompt.abortPrompt(); + } + }); + + try { + PromptUtils.fireDialogEvent(window, "DOMWillOpenModalDialog", browser); + + args.promptActive = true; + + newPrompt = tabPrompt.appendPrompt(args, onPromptClose); + + // TODO since we don't actually open a window, need to check if + // there's other stuff in nsWindowWatcher::OpenWindowInternal + // that we might need to do here as well. + } catch (ex) { + onPromptClose(true); + } + }, + + openModalWindow: function(args, browser) { + let window = browser.ownerDocument.defaultView; + try { + PromptUtils.fireDialogEvent(window, "DOMWillOpenModalDialog", browser); + let bag = PromptUtils.objectToPropBag(args); + + Services.ww.openWindow(window, args.uri, "_blank", + "centerscreen,chrome,modal,titlebar", bag); + + PromptUtils.propBagToObject(bag, args); + } finally { + PromptUtils.fireDialogEvent(window, "DOMModalDialogClosed", browser); + browser.messageManager.sendAsyncMessage("Prompt:Close", args); + } + } +}; diff --git a/browser/modules/moz.build b/browser/modules/moz.build index e3405b8f3778..c75581985814 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -15,6 +15,7 @@ EXTRA_JS_MODULES += [ 'Feeds.jsm', 'NetworkPrioritizer.jsm', 'offlineAppCache.jsm', + 'RemotePrompt.jsm', 'SharedFrame.jsm', 'SitePermissions.jsm', 'Social.jsm', diff --git a/browser/themes/shared/devtools/layoutview.css b/browser/themes/shared/devtools/layoutview.css index 355af98a8561..30bd731504ed 100644 --- a/browser/themes/shared/devtools/layoutview.css +++ b/browser/themes/shared/devtools/layoutview.css @@ -39,3 +39,16 @@ background-color: #d89b28; opacity: 0.6; } + +.editable { + border-bottom: 1px dashed transparent; +} + +.editable:hover { + border-bottom-color: hsl(0,0%,50%); +} + +.styleinspector-propertyeditor { + border: 1px solid #CCC; + padding: 0; +} diff --git a/browser/themes/shared/in-content/preferences.css b/browser/themes/shared/in-content/preferences.css index 5b25d2a2e3f0..c4ea519622f4 100644 --- a/browser/themes/shared/in-content/preferences.css +++ b/browser/themes/shared/in-content/preferences.css @@ -132,7 +132,6 @@ button, menulist { -moz-appearance: none; height: 30px; - max-height: 30px; color: #737980; line-height: 20px; text-shadow: 0 1px 1px #FEFFFE; diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index 879d44312b1d..c6b33c860035 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -123,21 +123,15 @@ * text and icons to not appear fuzzy. */ @media (-moz-windows-classic) { - #main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabbrowser-tab[selected=true] { - position: relative; - z-index: 3; - } - - #main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabbrowser-tab:not([selected=true]) { + /** + * We need to bump up the z-index of the tabbrowser-tabs so that they appear + * over top of the fog we're applying for classic themes, as well as the nav-bar. + */ + #main-window[tabsintitlebar]:not([sizemode=fullscreen]) #tabbrowser-tabs { position: relative; z-index: 2; } - #main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabs-newtab-button { - position: relative; - z-index: 1; - } - #main-window[tabsintitlebar] #TabsToolbar:not(:-moz-lwtheme) { background-image: none; position: relative; @@ -159,7 +153,6 @@ pointer-events: none; top: 100%; width: -moz-available; - z-index: 0; } #main-window[tabsintitlebar]:not([sizemode=fullscreen]) #TabsToolbar:not(:-moz-lwtheme):-moz-window-inactive::after { @@ -176,17 +169,31 @@ overflow: -moz-hidden-unscrollable; } - #main-window[tabsintitlebar][sizemode=normal] .tabbrowser-arrowscrollbox > .arrowscrollbox-scrollbox > .scrollbox-innerbox:not(:-moz-lwtheme) { - position: relative; + /** + * When the tabstrip is overflowed, pinned tab separators get position: absolute, + * which makes the pinned tab separators leak over the nav-bar highlight. Forcing + * the element to snap to the bottom of the client rect works around the issue. + */ + #main-window[tabsintitlebar] #tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned]::before { + bottom: 0px; } - #main-window[tabsintitlebar]:not([sizemode=fullscreen]) #TabsToolbar .toolbarbutton-1, - #main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabbrowser-arrowscrollbox > .scrollbutton-up, - #main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabbrowser-arrowscrollbox > .scrollbutton-down { + #main-window[tabsintitlebar]:not([sizemode=fullscreen]) #TabsToolbar .toolbarbutton-1 { position: relative; z-index: 1; } + /** + * With the tabbrowser-tabs element z-index'd above the nav-bar, we now get the + * scrollbox button borders leaking over the nav-bar highlight. This transparent bottom + * border forces the scrollbox button borders to terminate a pixel early, working + * around the issue. + */ + #main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabbrowser-arrowscrollbox > .scrollbutton-up, + #main-window[tabsintitlebar]:not([sizemode=fullscreen]) .tabbrowser-arrowscrollbox > .scrollbutton-down { + border-bottom: 1px solid transparent; + } + #main-window[tabsintitlebar]:not([inFullscreen]) .tab-close-button:not(:-moz-any(:hover,:-moz-lwtheme,[selected="true"])) { -moz-image-region: rect(0, 64px, 16px, 48px); } diff --git a/content/base/public/Element.h b/content/base/public/Element.h index 1f62964f2d41..197bffda9518 100644 --- a/content/base/public/Element.h +++ b/content/base/public/Element.h @@ -632,6 +632,32 @@ public: GetElementsByClassName(const nsAString& aClassNames); bool MozMatchesSelector(const nsAString& aSelector, ErrorResult& aError); + void SetPointerCapture(int32_t aPointerId, ErrorResult& aError) + { + bool activeState = false; + if (!nsIPresShell::GetPointerInfo(aPointerId, activeState)) { + aError.Throw(NS_ERROR_DOM_INVALID_POINTER_ERR); + return; + } + if (!activeState) { + return; + } + nsIPresShell::SetPointerCapturingContent(aPointerId, this); + } + void ReleasePointerCapture(int32_t aPointerId, ErrorResult& aError) + { + bool activeState = false; + if (!nsIPresShell::GetPointerInfo(aPointerId, activeState)) { + aError.Throw(NS_ERROR_DOM_INVALID_POINTER_ERR); + return; + } + + // Ignoring ReleasePointerCapture call on incorrect element (on element + // that didn't have capture before). + if (nsIPresShell::GetPointerCapturingContent(aPointerId) == this) { + nsIPresShell::ReleasePointerCapturingContent(aPointerId, this); + } + } void SetCapture(bool aRetargetToElement) { // If there is already an active capture, ignore this request. This would diff --git a/content/base/test/chrome/test_bug339494.xul b/content/base/test/chrome/test_bug339494.xul index d39019a2a24a..a81838450b85 100644 --- a/content/base/test/chrome/test_bug339494.xul +++ b/content/base/test/chrome/test_bug339494.xul @@ -5,11 +5,9 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=339494 --> - Test for Bug 339494 diff --git a/content/base/test/chrome/test_bug357450.xul b/content/base/test/chrome/test_bug357450.xul index ab996defbb4b..8d2d2845523b 100644 --- a/content/base/test/chrome/test_bug357450.xul +++ b/content/base/test/chrome/test_bug357450.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=357450 --> - Test for Bug 357450 diff --git a/content/base/test/chrome/test_fileconstructor.xul b/content/base/test/chrome/test_fileconstructor.xul index b32f1ef48201..8ace31e8b728 100644 --- a/content/base/test/chrome/test_fileconstructor.xul +++ b/content/base/test/chrome/test_fileconstructor.xul @@ -10,7 +10,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=607114.xul xmlns:html="http://www.w3.org/1999/xhtml" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - Test for Bug 607114 diff --git a/content/canvas/src/WebGLActiveInfo.h b/content/canvas/src/WebGLActiveInfo.h index 4c62fcffba3e..f6b262987a20 100644 --- a/content/canvas/src/WebGLActiveInfo.h +++ b/content/canvas/src/WebGLActiveInfo.h @@ -39,7 +39,12 @@ public: NS_INLINE_DECL_REFCOUNTING(WebGLActiveInfo) -protected: +private: + // Private destructor, to discourage deletion outside of Release(): + ~WebGLActiveInfo() + { + } + GLint mSize; GLenum mType; nsString mName; diff --git a/content/canvas/src/WebGLShaderPrecisionFormat.h b/content/canvas/src/WebGLShaderPrecisionFormat.h index 9645552b291e..9215eecf8ef3 100644 --- a/content/canvas/src/WebGLShaderPrecisionFormat.h +++ b/content/canvas/src/WebGLShaderPrecisionFormat.h @@ -39,7 +39,12 @@ public: NS_INLINE_DECL_REFCOUNTING(WebGLShaderPrecisionFormat) -protected: +private: + // Private destructor, to discourage deletion outside of Release(): + ~WebGLShaderPrecisionFormat() + { + } + GLint mRangeMin; GLint mRangeMax; GLint mPrecision; diff --git a/content/xul/content/test/test_bug233643.xul b/content/xul/content/test/test_bug233643.xul index d850b3dd5ad9..a1cf22bc81ee 100644 --- a/content/xul/content/test/test_bug233643.xul +++ b/content/xul/content/test/test_bug233643.xul @@ -2,7 +2,7 @@ + xmlns:html="http://www.w3.org/1999/xhtml" title="Test for Bug 233643"> diff --git a/content/xul/document/test/test_bug497875.xul b/content/xul/document/test/test_bug497875.xul index b142ffd60fef..a083daa53712 100644 --- a/content/xul/document/test/test_bug497875.xul +++ b/content/xul/document/test/test_bug497875.xul @@ -9,7 +9,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=497875 xmlns:html="http://www.w3.org/1999/xhtml" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - Test for Bug 497875 diff --git a/docshell/test/chrome/test_bug112564.xul b/docshell/test/chrome/test_bug112564.xul index e33c2d1386a9..0706595d0bd7 100644 --- a/docshell/test/chrome/test_bug112564.xul +++ b/docshell/test/chrome/test_bug112564.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=112564 --> - Test for Bug 112564 diff --git a/docshell/test/chrome/test_bug215405.xul b/docshell/test/chrome/test_bug215405.xul index b35f9424a163..e934d76cafbe 100644 --- a/docshell/test/chrome/test_bug215405.xul +++ b/docshell/test/chrome/test_bug215405.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=215405 --> - Test for Bug 215405 diff --git a/docshell/test/chrome/test_bug293235.xul b/docshell/test/chrome/test_bug293235.xul index 18c7348af4fe..8a42c0a6e70f 100644 --- a/docshell/test/chrome/test_bug293235.xul +++ b/docshell/test/chrome/test_bug293235.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=293235.xul --> - Test for Bug 293235 diff --git a/docshell/test/chrome/test_bug294258.xul b/docshell/test/chrome/test_bug294258.xul index 8d98c5a0b110..4658a82de035 100644 --- a/docshell/test/chrome/test_bug294258.xul +++ b/docshell/test/chrome/test_bug294258.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=294258.xul --> - Test for Bug 294258 diff --git a/docshell/test/chrome/test_bug298622.xul b/docshell/test/chrome/test_bug298622.xul index 5fad99361415..c3217c7e5920 100644 --- a/docshell/test/chrome/test_bug298622.xul +++ b/docshell/test/chrome/test_bug298622.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=298622.xul --> - Test for Bug 298622 diff --git a/docshell/test/chrome/test_bug301397.xul b/docshell/test/chrome/test_bug301397.xul index 8f22c191894d..37fe479932b0 100644 --- a/docshell/test/chrome/test_bug301397.xul +++ b/docshell/test/chrome/test_bug301397.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=301397.xul --> - Test for Bug 301397 diff --git a/docshell/test/chrome/test_bug303267.xul b/docshell/test/chrome/test_bug303267.xul index 28f5f451ebb9..f68dbdcd242d 100644 --- a/docshell/test/chrome/test_bug303267.xul +++ b/docshell/test/chrome/test_bug303267.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=303267.xul --> - Test for Bug 303267 diff --git a/docshell/test/chrome/test_bug311007.xul b/docshell/test/chrome/test_bug311007.xul index 9325ac07de82..32f1085f0027 100644 --- a/docshell/test/chrome/test_bug311007.xul +++ b/docshell/test/chrome/test_bug311007.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=311007.xul --> - Test for Bug 311007 diff --git a/docshell/test/chrome/test_bug360511.xul b/docshell/test/chrome/test_bug360511.xul index c4b9045d84cc..ffa47fa276a8 100644 --- a/docshell/test/chrome/test_bug360511.xul +++ b/docshell/test/chrome/test_bug360511.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=360511.xul --> - Test for Bug 360511 diff --git a/docshell/test/chrome/test_bug364461.xul b/docshell/test/chrome/test_bug364461.xul index a43cfca1bbc2..85154f9d74af 100644 --- a/docshell/test/chrome/test_bug364461.xul +++ b/docshell/test/chrome/test_bug364461.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=364461 --> - Test for Bug 364461 diff --git a/docshell/test/chrome/test_bug396649.xul b/docshell/test/chrome/test_bug396649.xul index ad9e4f557003..3554c2f98a57 100644 --- a/docshell/test/chrome/test_bug396649.xul +++ b/docshell/test/chrome/test_bug396649.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=396649.xul --> - Test for Bug 396649 diff --git a/docshell/test/chrome/test_bug565388.xul b/docshell/test/chrome/test_bug565388.xul index f39745deaf52..8cf1bbb80b89 100644 --- a/docshell/test/chrome/test_bug565388.xul +++ b/docshell/test/chrome/test_bug565388.xul @@ -7,7 +7,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=565388 - Test for Bug 565388 diff --git a/docshell/test/chrome/test_bug582176.xul b/docshell/test/chrome/test_bug582176.xul index e72da288b2bd..98053b009a99 100644 --- a/docshell/test/chrome/test_bug582176.xul +++ b/docshell/test/chrome/test_bug582176.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=582176.xul --> - Test for Bug 582176 diff --git a/docshell/test/chrome/test_bug662200.xul b/docshell/test/chrome/test_bug662200.xul index 214ac6ca1e6f..8b864c8afead 100644 --- a/docshell/test/chrome/test_bug662200.xul +++ b/docshell/test/chrome/test_bug662200.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=662200.xul --> - Test for Bug 662200 diff --git a/docshell/test/chrome/test_bug846906.xul b/docshell/test/chrome/test_bug846906.xul index 955e58b29c96..066c75224ef3 100644 --- a/docshell/test/chrome/test_bug846906.xul +++ b/docshell/test/chrome/test_bug846906.xul @@ -7,7 +7,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=846906 - Test for Bug 846906 diff --git a/docshell/test/chrome/test_bug89419.xul b/docshell/test/chrome/test_bug89419.xul index b825a0bbea1e..26194c49b372 100644 --- a/docshell/test/chrome/test_bug89419.xul +++ b/docshell/test/chrome/test_bug89419.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=89419.xul --> - Test for Bug 89419 diff --git a/docshell/test/chrome/test_bug92598.xul b/docshell/test/chrome/test_bug92598.xul index 6f3623bb671a..8e81463be63d 100644 --- a/docshell/test/chrome/test_bug92598.xul +++ b/docshell/test/chrome/test_bug92598.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=92598 --> - Test for Bug 92598 diff --git a/docshell/test/chrome/test_mozFrameType.xul b/docshell/test/chrome/test_mozFrameType.xul index f65863f77123..225cd16cc252 100644 --- a/docshell/test/chrome/test_mozFrameType.xul +++ b/docshell/test/chrome/test_mozFrameType.xul @@ -9,10 +9,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=769771 --> - Test mozFrameType attribute diff --git a/docshell/test/chrome/test_principalInherit.xul b/docshell/test/chrome/test_principalInherit.xul index 2d79f337a551..87056c63dccf 100644 --- a/docshell/test/chrome/test_principalInherit.xul +++ b/docshell/test/chrome/test_principalInherit.xul @@ -9,10 +9,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=719994 --> - Test principal inheriting diff --git a/dom/base/DOMException.cpp b/dom/base/DOMException.cpp index af5b6ed7b7d4..1c2f07a00f0a 100644 --- a/dom/base/DOMException.cpp +++ b/dom/base/DOMException.cpp @@ -50,6 +50,7 @@ enum DOM4ErrorTypeCodeMap { TimeoutError = nsIDOMDOMException::TIMEOUT_ERR, InvalidNodeTypeError = nsIDOMDOMException::INVALID_NODE_TYPE_ERR, DataCloneError = nsIDOMDOMException::DATA_CLONE_ERR, + InvalidPointerId = nsIDOMDOMException::INVALID_POINTER_ERR, EncodingError = 0, /* XXX Should be JavaScript native errors */ diff --git a/dom/base/domerr.msg b/dom/base/domerr.msg index ab59ce5845b0..e5bae9e7b2d5 100644 --- a/dom/base/domerr.msg +++ b/dom/base/domerr.msg @@ -30,6 +30,7 @@ DOM4_MSG_DEF(QuotaExceededError, "The quota has been exceeded.", NS_ERROR_DOM_QU DOM4_MSG_DEF(TimeoutError, "The operation timed out.", NS_ERROR_DOM_TIMEOUT_ERR) DOM4_MSG_DEF(InvalidNodeTypeError, "The supplied node is incorrect or has an incorrect ancestor for this operation.", NS_ERROR_DOM_INVALID_NODE_TYPE_ERR) DOM4_MSG_DEF(DataCloneError, "The object could not be cloned.", NS_ERROR_DOM_DATA_CLONE_ERR) +DOM4_MSG_DEF(InvalidPointerId, "Invalid pointer id.", NS_ERROR_DOM_INVALID_POINTER_ERR) /* XXX Should be JavaScript native TypeError */ DOM4_MSG_DEF(TypeError, "The method parameter is missing or invalid.", NS_ERROR_TYPE_ERR) diff --git a/dom/base/nsDOMWindowUtils.cpp b/dom/base/nsDOMWindowUtils.cpp index 1958f4b87963..29118e38c943 100644 --- a/dom/base/nsDOMWindowUtils.cpp +++ b/dom/base/nsDOMWindowUtils.cpp @@ -67,6 +67,7 @@ #include "mozilla/dom/Element.h" #include "mozilla/dom/file/FileHandle.h" #include "mozilla/dom/FileHandleBinding.h" +#include "mozilla/dom/TabChild.h" #include "mozilla/dom/IDBFactoryBinding.h" #include "mozilla/dom/indexedDB/IndexedDatabaseManager.h" #include "mozilla/dom/quota/PersistenceType.h" @@ -3623,6 +3624,12 @@ nsDOMWindowUtils::GetIsParentWindowMainWidgetVisible(bool* aIsVisible) nsCOMPtr parentWidget; nsIDocShell *docShell = window->GetDocShell(); if (docShell) { + if (TabChild *tabChild = TabChild::GetFrom(docShell)) { + if (!tabChild->SendIsParentWindowMainWidgetVisible(aIsVisible)) + return NS_ERROR_FAILURE; + return NS_OK; + } + nsCOMPtr parentTreeOwner; docShell->GetTreeOwner(getter_AddRefs(parentTreeOwner)); nsCOMPtr parentWindow(do_GetInterface(parentTreeOwner)); diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp index 3dbaf412817b..6e8cb13a2d96 100644 --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -2917,21 +2917,6 @@ nsGlobalWindow::UpdateParentTarget() mParentTarget = eventTarget; } -bool -nsGlobalWindow::GetIsTabModalPromptAllowed() -{ - MOZ_ASSERT(IsOuterWindow()); - - bool allowTabModal = true; - if (mDocShell) { - nsCOMPtr cv; - mDocShell->GetContentViewer(getter_AddRefs(cv)); - cv->GetIsTabModalPromptAllowed(&allowTabModal); - } - - return allowTabModal; -} - EventTarget* nsGlobalWindow::GetTargetForDOMEvent() { @@ -6166,10 +6151,6 @@ nsGlobalWindow::AlertOrConfirm(bool aAlert, nsAutoString final; nsContentUtils::StripNullChars(aMessage, final); - // Check if we're being called at a point where we can't use tab-modal - // prompts, because something doesn't want reentrancy. - bool allowTabModal = GetIsTabModalPromptAllowed(); - nsresult rv; nsCOMPtr promptFac = do_GetService("@mozilla.org/prompter;1", &rv); @@ -6185,9 +6166,10 @@ nsGlobalWindow::AlertOrConfirm(bool aAlert, return false; } - nsCOMPtr promptBag = do_QueryInterface(prompt); - if (promptBag) - promptBag->SetPropertyAsBool(NS_LITERAL_STRING("allowTabModal"), allowTabModal); + // Always allow tab modal prompts for alert and confirm. + if (nsCOMPtr promptBag = do_QueryInterface(prompt)) { + promptBag->SetPropertyAsBool(NS_LITERAL_STRING("allowTabModal"), true); + } bool result = false; nsAutoSyncOperation sync(mDoc); @@ -6281,10 +6263,6 @@ nsGlobalWindow::Prompt(const nsAString& aMessage, const nsAString& aInitial, nsContentUtils::StripNullChars(aMessage, fixedMessage); nsContentUtils::StripNullChars(aInitial, fixedInitial); - // Check if we're being called at a point where we can't use tab-modal - // prompts, because something doesn't want reentrancy. - bool allowTabModal = GetIsTabModalPromptAllowed(); - nsresult rv; nsCOMPtr promptFac = do_GetService("@mozilla.org/prompter;1", &rv); @@ -6300,9 +6278,10 @@ nsGlobalWindow::Prompt(const nsAString& aMessage, const nsAString& aInitial, return; } - nsCOMPtr promptBag = do_QueryInterface(prompt); - if (promptBag) - promptBag->SetPropertyAsBool(NS_LITERAL_STRING("allowTabModal"), allowTabModal); + // Always allow tab modal prompts for prompt. + if (nsCOMPtr promptBag = do_QueryInterface(prompt)) { + promptBag->SetPropertyAsBool(NS_LITERAL_STRING("allowTabModal"), true); + } // Pass in the default value, if any. char16_t *inoutValue = ToNewUnicode(fixedInitial); diff --git a/dom/base/nsGlobalWindow.h b/dom/base/nsGlobalWindow.h index 53fe677311bc..7db1b2c4fc64 100644 --- a/dom/base/nsGlobalWindow.h +++ b/dom/base/nsGlobalWindow.h @@ -1299,9 +1299,6 @@ protected: virtual void UpdateParentTarget(); - // Outer windows only. - bool GetIsTabModalPromptAllowed(); - inline int32_t DOMMinTimeoutValue() const; nsresult CloneStorageEvent(const nsAString& aType, diff --git a/dom/base/test/test_domrequesthelper.xul b/dom/base/test/test_domrequesthelper.xul index ea2a704fa747..8d7ffbaf5db0 100644 --- a/dom/base/test/test_domrequesthelper.xul +++ b/dom/base/test/test_domrequesthelper.xul @@ -9,7 +9,6 @@ - DOMRequestHelper Test diff --git a/dom/plugins/test/mochitest/test_convertpoint.xul b/dom/plugins/test/mochitest/test_convertpoint.xul index e1fb3b26c3aa..fcfe59431597 100644 --- a/dom/plugins/test/mochitest/test_convertpoint.xul +++ b/dom/plugins/test/mochitest/test_convertpoint.xul @@ -3,9 +3,7 @@ - NPN_ConvertPoint Tests diff --git a/dom/plugins/test/mochitest/test_crash_notify.xul b/dom/plugins/test/mochitest/test_crash_notify.xul index fdd2bfebd147..ce5f3b040c7d 100644 --- a/dom/plugins/test/mochitest/test_crash_notify.xul +++ b/dom/plugins/test/mochitest/test_crash_notify.xul @@ -3,9 +3,7 @@ - Plugin Crash Notification Test diff --git a/dom/plugins/test/mochitest/test_crash_notify_no_report.xul b/dom/plugins/test/mochitest/test_crash_notify_no_report.xul index 2103bd1e1909..0b5c914259c7 100644 --- a/dom/plugins/test/mochitest/test_crash_notify_no_report.xul +++ b/dom/plugins/test/mochitest/test_crash_notify_no_report.xul @@ -3,9 +3,7 @@ - Plugin Crash Notification Test diff --git a/dom/plugins/test/mochitest/test_crash_submit.xul b/dom/plugins/test/mochitest/test_crash_submit.xul index ba8044f38e23..41d346d5b10a 100644 --- a/dom/plugins/test/mochitest/test_crash_submit.xul +++ b/dom/plugins/test/mochitest/test_crash_submit.xul @@ -3,9 +3,7 @@ - Plugin Crash Notification Test diff --git a/dom/plugins/test/mochitest/test_privatemode_perwindowpb.xul b/dom/plugins/test/mochitest/test_privatemode_perwindowpb.xul index c3c42074acb4..b325ba4e383e 100644 --- a/dom/plugins/test/mochitest/test_privatemode_perwindowpb.xul +++ b/dom/plugins/test/mochitest/test_privatemode_perwindowpb.xul @@ -3,9 +3,7 @@ - NPAPI Private Mode Tests diff --git a/dom/tests/mochitest/chrome/test_bug830396.xul b/dom/tests/mochitest/chrome/test_bug830396.xul index de7a25f45c6d..e5f32930f797 100644 --- a/dom/tests/mochitest/chrome/test_bug830396.xul +++ b/dom/tests/mochitest/chrome/test_bug830396.xul @@ -6,10 +6,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=830396 --> - Test for Bug 830396 + + + + +Mozilla Bug 968148 +

+ +
+
+
+
 

 
+
 

 
+

+
+
diff --git a/layout/base/tests/mochitest.ini b/layout/base/tests/mochitest.ini
index d02116479d70..a3eb4a773400 100644
--- a/layout/base/tests/mochitest.ini
+++ b/layout/base/tests/mochitest.ini
@@ -178,6 +178,8 @@ skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s # b2g(depends on plu
 [test_bug583889.html]
 support-files = bug583889_inner1.html bug583889_inner2.html
 [test_bug582771.html]
+[test_bug968148.html]
+support-files = bug968148_inner.html
 [test_bug603550.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s #TIMED_OUT # b2g(Components.classes[@mozilla.org/widget/dragservice;1] is undefined) b2g-debug(Components.classes[@mozilla.org/widget/dragservice;1] is undefined) b2g-desktop(Components.classes[@mozilla.org/widget/dragservice;1] is undefined)
 [test_bug629838.html]
diff --git a/layout/base/tests/test_bug968148.html b/layout/base/tests/test_bug968148.html
new file mode 100644
index 000000000000..3cab8b073c9f
--- /dev/null
+++ b/layout/base/tests/test_bug968148.html
@@ -0,0 +1,36 @@
+
+
+
+
+  Test for Bug 968148
+  
+  
+  
+
+
+  
+
+
diff --git a/layout/forms/test/bug665540_window.xul b/layout/forms/test/bug665540_window.xul
index 7075adf458ef..f0225ef9363b 100644
--- a/layout/forms/test/bug665540_window.xul
+++ b/layout/forms/test/bug665540_window.xul
@@ -5,8 +5,6 @@
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         sizemode="fullscreen">
 
-  <label>Test Select Dropdown Positioning in Fullscreen Window</label>
-
   
diff --git a/layout/generic/test/test_bug469613.xul b/layout/generic/test/test_bug469613.xul
index 9d0b75136a30..d15ec4732f32 100644
--- a/layout/generic/test/test_bug469613.xul
+++ b/layout/generic/test/test_bug469613.xul
@@ -7,7 +7,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=469613
 
 
-  Test for Bug 469613
   
 
diff --git a/layout/generic/test/test_bug508115.xul b/layout/generic/test/test_bug508115.xul
index 9c55c96f4aa4..576187a8e851 100644
--- a/layout/generic/test/test_bug508115.xul
+++ b/layout/generic/test/test_bug508115.xul
@@ -9,7 +9,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=508115
   xmlns:html="http://www.w3.org/1999/xhtml"
   xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
-  Test for Bug 508115
   
diff --git a/layout/generic/test/test_bug632379.xul b/layout/generic/test/test_bug632379.xul
index bbece27415ba..4a5292e1b037 100644
--- a/layout/generic/test/test_bug632379.xul
+++ b/layout/generic/test/test_bug632379.xul
@@ -7,7 +7,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=632379
 
 
-  Test for Bug 632379
   
 
diff --git a/layout/style/nsFontFaceLoader.h b/layout/style/nsFontFaceLoader.h
index 0a6f0686edfc..7a1f063bf9ea 100644
--- a/layout/style/nsFontFaceLoader.h
+++ b/layout/style/nsFontFaceLoader.h
@@ -28,7 +28,6 @@ class nsUserFontSet : public gfxUserFontSet
 {
 public:
   nsUserFontSet(nsPresContext* aContext);
-  ~nsUserFontSet();
 
   // Called when this font set is no longer associated with a presentation.
   void Destroy();
@@ -54,6 +53,10 @@ public:
   nsCSSFontFaceRule* FindRuleForEntry(gfxFontEntry* aFontEntry);
 
 protected:
+  // Protected destructor, to discourage deletion outside of Release()
+  // (since we inherit from refcounted class gfxUserFontSet):
+  ~nsUserFontSet();
+
   // The font-set keeps track of the collection of rules, and their
   // corresponding font entries (whether proxies or real entries),
   // so that we can update the set without having to throw away
diff --git a/layout/xul/nsXULPopupManager.cpp b/layout/xul/nsXULPopupManager.cpp
index 3c2c6867e0fa..5cfaf87ba85d 100644
--- a/layout/xul/nsXULPopupManager.cpp
+++ b/layout/xul/nsXULPopupManager.cpp
@@ -393,28 +393,32 @@ nsXULPopupManager::PopupMoved(nsIFrame* aFrame, nsIntPoint aPnt)
   if (!menuPopupFrame)
     return;
 
-  // Convert desired point to CSS pixels for comparison
-  nsPresContext* presContext = menuPopupFrame->PresContext();
-  aPnt.x = presContext->DevPixelsToIntCSSPixels(aPnt.x);
-  aPnt.y = presContext->DevPixelsToIntCSSPixels(aPnt.y);
+  nsView* view = menuPopupFrame->GetView();
+  if (!view)
+    return;
 
   // Don't do anything if the popup is already at the specified location. This
   // prevents recursive calls when a popup is positioned.
-  nsIntPoint currentPnt = menuPopupFrame->ScreenPosition();
+  nsIntRect curDevSize = view->CalcWidgetBounds(eWindowType_popup);
   nsIWidget* widget = menuPopupFrame->GetWidget();
-  if ((aPnt.x != currentPnt.x || aPnt.y != currentPnt.y) || (widget &&
-      widget->GetClientOffset() != menuPopupFrame->GetLastClientOffset())) {
-    // Update the popup's position using SetPopupPosition if the popup is
-    // anchored and at the parent level as these maintain their position
-    // relative to the parent window. Otherwise, just update the popup to
-    // the specified screen coordinates.
-    if (menuPopupFrame->IsAnchored() &&
-        menuPopupFrame->PopupLevel() == ePopupLevelParent) {
-      menuPopupFrame->SetPopupPosition(nullptr, true, false);
-    }
-    else {
-      menuPopupFrame->MoveTo(aPnt.x, aPnt.y, false);
-    }
+  if (curDevSize.x == aPnt.x && curDevSize.y == aPnt.y &&
+      (!widget || widget->GetClientOffset() == menuPopupFrame->GetLastClientOffset())) {
+    return;
+  }
+
+  // Update the popup's position using SetPopupPosition if the popup is
+  // anchored and at the parent level as these maintain their position
+  // relative to the parent window. Otherwise, just update the popup to
+  // the specified screen coordinates.
+  if (menuPopupFrame->IsAnchored() &&
+      menuPopupFrame->PopupLevel() == ePopupLevelParent) {
+    menuPopupFrame->SetPopupPosition(nullptr, true, false);
+  }
+  else {
+    nsPresContext* presContext = menuPopupFrame->PresContext();
+    aPnt.x = presContext->DevPixelsToIntCSSPixels(aPnt.x);
+    aPnt.y = presContext->DevPixelsToIntCSSPixels(aPnt.y);
+    menuPopupFrame->MoveTo(aPnt.x, aPnt.y, false);
   }
 }
 
diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java
index c8d5d37ba9e1..dd8b1721cae8 100644
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -567,6 +567,7 @@ abstract public class BrowserApp extends GeckoApp
         registerEventListener("Menu:Update");
         registerEventListener("Accounts:Create");
         registerEventListener("Accounts:Exist");
+        registerEventListener("Prompt:ShowTop");
 
         Distribution.init(this);
         JavaAddonManager.getInstance().init(getApplicationContext());
diff --git a/mobile/android/base/EventDispatcher.java b/mobile/android/base/EventDispatcher.java
index dfeec4260f14..2359746f4c85 100644
--- a/mobile/android/base/EventDispatcher.java
+++ b/mobile/android/base/EventDispatcher.java
@@ -7,12 +7,17 @@ package org.mozilla.gecko;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSContainer;
 
+import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.util.Log;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 
@@ -22,86 +27,171 @@ public final class EventDispatcher {
     private static final String SUFFIX_RETURN = "Return";
     private static final String SUFFIX_ERROR = "Error";
 
-    private final Map> mEventListeners
-                  = new HashMap>();
+    /**
+     * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size
+     * of the map goes beyond 75% of the capacity, the map is rehashed. Therefore, to
+     * empirically determine the initial capacity that avoids rehashing, we need to
+     * determine the initial size, divide it by 75%, and round up to the next power-of-2.
+     */
+    private static final int GECKO_NATIVE_EVENTS_COUNT = 0; // Default for HashMap
+    private static final int GECKO_JSON_EVENTS_COUNT = 256; // Empirically measured
 
-    public void registerEventListener(String event, GeckoEventListener listener) {
-        synchronized (mEventListeners) {
-            CopyOnWriteArrayList listeners = mEventListeners.get(event);
-            if (listeners == null) {
-                // create a CopyOnWriteArrayList so that we can modify it
-                // concurrently with iterating through it in handleGeckoMessage.
-                // Otherwise we could end up throwing a ConcurrentModificationException.
-                listeners = new CopyOnWriteArrayList();
-            } else if (listeners.contains(listener)) {
-                Log.w(LOGTAG, "EventListener already registered for event '" + event + "'",
-                      new IllegalArgumentException());
+    private final Map> mGeckoThreadNativeListeners =
+        new HashMap>(GECKO_NATIVE_EVENTS_COUNT);
+    private final Map> mGeckoThreadJSONListeners =
+        new HashMap>(GECKO_JSON_EVENTS_COUNT);
+
+    private  void registerListener(final Class> listType,
+                                      final Map> listenersMap,
+                                      final T listener,
+                                      final String[] events) {
+        try {
+            synchronized (listenersMap) {
+                for (final String event : events) {
+                    List listeners = listenersMap.get(event);
+                    if (listeners == null) {
+                        listeners = listType.newInstance();
+                        listenersMap.put(event, listeners);
+                    }
+                    if (!AppConstants.RELEASE_BUILD && listeners.contains(listener)) {
+                        throw new IllegalStateException("Already registered " + event);
+                    }
+                    listeners.add(listener);
+                }
             }
-            listeners.add(listener);
-            mEventListeners.put(event, listeners);
+        } catch (final IllegalAccessException e) {
+            throw new IllegalArgumentException("Invalid new list type", e);
+        } catch (final InstantiationException e) {
+            throw new IllegalArgumentException("Invalid new list type", e);
         }
     }
 
-    public void unregisterEventListener(String event, GeckoEventListener listener) {
-        synchronized (mEventListeners) {
-            CopyOnWriteArrayList listeners = mEventListeners.get(event);
-            if (listeners == null) {
-                Log.w(LOGTAG, "unregisterEventListener: event '" + event + "' has no listeners");
+    private  void checkNotRegistered(final Map> listenersMap,
+                                        final String[] events) {
+        synchronized (listenersMap) {
+            for (final String event: events) {
+                if (listenersMap.get(event) != null) {
+                    throw new IllegalStateException(
+                        "Already registered " + event + " under a different type");
+                }
+            }
+        }
+    }
+
+    private  void unregisterListener(final Map> listenersMap,
+                                        final T listener,
+                                        final String[] events) {
+        synchronized (listenersMap) {
+            for (final String event : events) {
+                List listeners = listenersMap.get(event);
+                if ((listeners == null ||
+                     !listeners.remove(listener)) && !AppConstants.RELEASE_BUILD) {
+                    throw new IllegalArgumentException(event + " was not registered");
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public void registerGeckoThreadListener(final NativeEventListener listener,
+                                            final String... events) {
+        checkNotRegistered(mGeckoThreadJSONListeners, events);
+
+        // For listeners running on the Gecko thread, we want to notify the listeners
+        // outside of our synchronized block, because the listeners may take an
+        // indeterminate amount of time to run. Therefore, to ensure concurrency when
+        // iterating the list outside of the synchronized block, we use a
+        // CopyOnWriteArrayList.
+        registerListener((Class)CopyOnWriteArrayList.class,
+                         mGeckoThreadNativeListeners, listener, events);
+    }
+
+    @Deprecated // Use NativeEventListener instead
+    @SuppressWarnings("unchecked")
+    private void registerGeckoThreadListener(final GeckoEventListener listener,
+                                             final String... events) {
+        checkNotRegistered(mGeckoThreadNativeListeners, events);
+
+        registerListener((Class)CopyOnWriteArrayList.class,
+                         mGeckoThreadJSONListeners, listener, events);
+    }
+
+    public void unregisterGeckoThreadListener(final NativeEventListener listener,
+                                              final String... events) {
+        unregisterListener(mGeckoThreadNativeListeners, listener, events);
+    }
+
+    @Deprecated // Use NativeEventListener instead
+    private void unregisterGeckoThreadListener(final GeckoEventListener listener,
+                                               final String... events) {
+        unregisterListener(mGeckoThreadJSONListeners, listener, events);
+    }
+
+    @Deprecated // Use one of the variants above.
+    public void registerEventListener(final String event, final GeckoEventListener listener) {
+        registerGeckoThreadListener(listener, event);
+    }
+
+    @Deprecated // Use one of the variants above
+    public void unregisterEventListener(final String event, final GeckoEventListener listener) {
+        unregisterGeckoThreadListener(listener, event);
+    }
+
+    public void dispatchEvent(final NativeJSContainer message) {
+        try {
+            // First try native listeners.
+            final String type = message.getString("type");
+
+            final List listeners;
+            synchronized (mGeckoThreadNativeListeners) {
+                listeners = mGeckoThreadNativeListeners.get(type);
+            }
+            if (listeners != null) {
+                if (listeners.size() == 0) {
+                    Log.w(LOGTAG, "No listeners for " + type);
+                }
+                for (final NativeEventListener listener : listeners) {
+                    listener.handleMessage(type, message);
+                }
+                // If we found native listeners, we assume we don't have any JSON listeners
+                // and return early. This assumption is checked when registering listeners.
                 return;
             }
-            if (!listeners.remove(listener)) {
-                Log.w(LOGTAG, "unregisterEventListener: tried to remove an unregistered listener " +
-                              "for event '" + event + "'");
-            }
-            if (listeners.size() == 0) {
-                mEventListeners.remove(event);
-            }
+        } catch (final IllegalArgumentException e) {
+            // Message doesn't have a "type" property, fallback to JSON
         }
-    }
-
-    public void dispatchEvent(String message) {
         try {
-            JSONObject json = new JSONObject(message);
-            dispatchEvent(json);
-        } catch (Exception e) {
-            Log.e(LOGTAG, "dispatchEvent: malformed JSON.", e);
+            // If we didn't find native listeners, try JSON listeners.
+            dispatchEvent(new JSONObject(message.toString()));
+        } catch (final JSONException e) {
+            Log.e(LOGTAG, "Cannot parse JSON");
+        } catch (final UnsupportedOperationException e) {
+            Log.e(LOGTAG, "Cannot convert message to JSON");
         }
     }
 
-    public void dispatchEvent(JSONObject json) {
+    public void dispatchEvent(final JSONObject message) {
         // {
         //   "type": "value",
         //   "event_specific": "value",
         //   ...
         try {
-            JSONObject gecko = json.has("gecko") ? json.getJSONObject("gecko") : null;
-            if (gecko != null) {
-                json = gecko;
+            final String type = message.getString("type");
+
+            List listeners;
+            synchronized (mGeckoThreadJSONListeners) {
+                listeners = mGeckoThreadJSONListeners.get(type);
             }
-
-            String type = json.getString("type");
-
-            if (gecko != null) {
-                Log.w(LOGTAG, "Message '" + type + "' has deprecated 'gecko' property!");
-            }
-
-            CopyOnWriteArrayList listeners;
-            synchronized (mEventListeners) {
-                listeners = mEventListeners.get(type);
-            }
-
             if (listeners == null || listeners.size() == 0) {
-                Log.d(LOGTAG, "dispatchEvent: no listeners registered for event '" + type + "'");
+                Log.w(LOGTAG, "No listeners for " + type);
                 return;
             }
-
-            for (GeckoEventListener listener : listeners) {
-                listener.handleMessage(type, json);
+            for (final GeckoEventListener listener : listeners) {
+                listener.handleMessage(type, message);
             }
-        } catch (Exception e) {
+        } catch (final JSONException e) {
             Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
         }
-
     }
 
     public static void sendResponse(JSONObject message, Object response) {
diff --git a/mobile/android/base/FindInPageBar.java b/mobile/android/base/FindInPageBar.java
index b2881a9d6842..41cf692331e4 100644
--- a/mobile/android/base/FindInPageBar.java
+++ b/mobile/android/base/FindInPageBar.java
@@ -85,6 +85,9 @@ public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnC
      }
 
     public void onDestroy() {
+        if (!mInflated) {
+            return;
+        }
         GeckoAppShell.getEventDispatcher().unregisterEventListener("TextSelection:Data", this);
     }
 
diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java
index e405b49a029c..83751cc49391 100644
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -45,6 +45,7 @@ import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
 import org.mozilla.gecko.prompts.PromptService;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.NativeJSContainer;
 import org.mozilla.gecko.util.ProxySelector;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.webapp.Allocator;
@@ -2287,8 +2288,9 @@ public class GeckoAppShell
     }
 
     @WrapElementForJNI(stubName = "HandleGeckoMessageWrapper")
-    public static void handleGeckoMessage(String message) {
+    public static void handleGeckoMessage(final NativeJSContainer message) {
         sEventDispatcher.dispatchEvent(message);
+        message.dispose();
     }
 
     @WrapElementForJNI
diff --git a/mobile/android/base/GeckoEditable.java b/mobile/android/base/GeckoEditable.java
index b9f9d31b595a..589547484de2 100644
--- a/mobile/android/base/GeckoEditable.java
+++ b/mobile/android/base/GeckoEditable.java
@@ -103,7 +103,8 @@ final class GeckoEditable
     private int mIcUpdateSeqno;
     private int mLastIcUpdateSeqno;
     private boolean mUpdateGecko;
-    private boolean mFocused;
+    private boolean mFocused; // Used by IC thread
+    private boolean mGeckoFocused; // Used by Gecko thread
     private volatile boolean mSuppressCompositions;
     private volatile boolean mSuppressKeyUp;
 
@@ -759,11 +760,16 @@ final class GeckoEditable
         });
 
         // Register/unregister Gecko-side text selection listeners
-        if (type == NOTIFY_IME_OF_BLUR) {
+        // and update the mGeckoFocused flag.
+        if (type == NOTIFY_IME_OF_BLUR && mGeckoFocused) {
+            // Check for focus here because Gecko may send us a blur before a focus in some
+            // cases, and we don't want to unregister an event that was not registered.
+            mGeckoFocused = false;
             mSuppressCompositions = false;
             GeckoAppShell.getEventDispatcher().
                 unregisterEventListener("TextSelection:IMECompositions", this);
         } else if (type == NOTIFY_IME_OF_FOCUS) {
+            mGeckoFocused = true;
             mSuppressCompositions = false;
             GeckoAppShell.getEventDispatcher().
                 registerEventListener("TextSelection:IMECompositions", this);
diff --git a/mobile/android/base/android-services.mozbuild b/mobile/android/base/android-services.mozbuild
index 61aa84ce746b..280915aaff77 100644
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -587,6 +587,7 @@ sync_java_files = [
     'fxa/sync/FxAccountSchedulePolicy.java',
     'fxa/sync/FxAccountSyncAdapter.java',
     'fxa/sync/FxAccountSyncService.java',
+    'fxa/sync/FxAccountSyncStatusHelper.java',
     'fxa/sync/SchedulePolicy.java',
     'sync/AlreadySyncingException.java',
     'sync/BackoffHandler.java',
diff --git a/mobile/android/base/fxa/FirefoxAccounts.java b/mobile/android/base/fxa/FirefoxAccounts.java
index bddce3b4e433..24206ede3e44 100644
--- a/mobile/android/base/fxa/FirefoxAccounts.java
+++ b/mobile/android/base/fxa/FirefoxAccounts.java
@@ -5,16 +5,21 @@
 package org.mozilla.gecko.fxa;
 
 import java.io.File;
+import java.util.EnumSet;
 import java.util.concurrent.CountDownLatch;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.fxa.authenticator.AccountPickler;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
 import org.mozilla.gecko.sync.ThreadPool;
+import org.mozilla.gecko.sync.Utils;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.os.Bundle;
 
 /**
  * Simple public accessors for Firefox account objects.
@@ -22,6 +27,40 @@ import android.content.Context;
 public class FirefoxAccounts {
   private static final String LOG_TAG = FirefoxAccounts.class.getSimpleName();
 
+  public enum SyncHint {
+    /**
+     * Hint that a requested sync is preferred immediately.
+     * 

+ * On many devices, not including SCHEDULE_NOW means a delay of + * at least 30 seconds. + */ + SCHEDULE_NOW, + + /** + * Hint that a requested sync may ignore local rate limiting. + *

+ * This is just a hint; the actual requested sync may not obey the hint. + */ + IGNORE_LOCAL_RATE_LIMIT, + + /** + * Hint that a requested sync may ignore remote server backoffs. + *

+ * This is just a hint; the actual requested sync may not obey the hint. + */ + IGNORE_REMOTE_SERVER_BACKOFF, + } + + public static final EnumSet SOON = EnumSet.noneOf(SyncHint.class); + + public static final EnumSet NOW = EnumSet.of( + SyncHint.SCHEDULE_NOW); + + public static final EnumSet FORCE = EnumSet.of( + SyncHint.SCHEDULE_NOW, + SyncHint.IGNORE_LOCAL_RATE_LIMIT, + SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); + /** * Returns true if a FirefoxAccount exists, false otherwise. * @@ -104,4 +143,86 @@ public class FirefoxAccounts { } return null; } + + protected static void putHintsToSync(final Bundle extras, EnumSet syncHints) { + // stagesToSync and stagesToSkip are allowed to be null. + if (syncHints == null) { + throw new IllegalArgumentException("syncHints must not be null"); + } + + final boolean scheduleNow = syncHints.contains(SyncHint.SCHEDULE_NOW); + final boolean ignoreLocalRateLimit = syncHints.contains(SyncHint.IGNORE_LOCAL_RATE_LIMIT); + final boolean ignoreRemoteServerBackoff = syncHints.contains(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); + + extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, scheduleNow); + // The default when manually syncing is to ignore the local rate limit and + // any remote server backoff requests. Since we can't add flags to a manual + // sync instigated by the user, we have to reverse the natural conditionals. + // See also the FORCE EnumSet. + extras.putBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT, !ignoreLocalRateLimit); + extras.putBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF, !ignoreRemoteServerBackoff); + } + + public static EnumSet getHintsToSyncFromBundle(final Bundle extras) { + final EnumSet syncHints = EnumSet.noneOf(SyncHint.class); + + final boolean scheduleNow = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); + final boolean ignoreLocalRateLimit = !extras.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT, false); + final boolean ignoreRemoteServerBackoff = !extras.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF, false); + + if (scheduleNow) { + syncHints.add(SyncHint.SCHEDULE_NOW); + } + if (ignoreLocalRateLimit) { + syncHints.add(SyncHint.IGNORE_LOCAL_RATE_LIMIT); + } + if (ignoreRemoteServerBackoff) { + syncHints.add(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); + } + + return syncHints; + } + + public static void logSyncHints(EnumSet syncHints) { + final boolean scheduleNow = syncHints.contains(SyncHint.SCHEDULE_NOW); + final boolean ignoreLocalRateLimit = syncHints.contains(SyncHint.IGNORE_LOCAL_RATE_LIMIT); + final boolean ignoreRemoteServerBackoff = syncHints.contains(SyncHint.IGNORE_REMOTE_SERVER_BACKOFF); + + Logger.info(LOG_TAG, "Sync hints" + + "; scheduling now: " + scheduleNow + + "; ignoring local rate limit: " + ignoreLocalRateLimit + + "; ignoring remote server backoff: " + ignoreRemoteServerBackoff + "."); + } + + /** + * Request a sync for the given Android Account. + *

+ * Any hints are strictly optional: the actual requested sync is scheduled by + * the Android sync scheduler, and the sync mechanism may ignore hints as it + * sees fit. + * + * @param account to sync. + * @param syncHints to pass to sync. + * @param stagesToSync stage names to sync. + * @param stagesToSkip stage names to skip. + */ + public static void requestSync(Account account, EnumSet syncHints, String[] stagesToSync, String[] stagesToSkip) { + if (account == null) { + throw new IllegalArgumentException("account must not be null"); + } + if (syncHints == null) { + throw new IllegalArgumentException("syncHints must not be null"); + } + + final Bundle extras = new Bundle(); + putHintsToSync(extras, syncHints); + Utils.putStageNamesToSync(extras, stagesToSync, stagesToSkip); + + Logger.info(LOG_TAG, "Requesting sync."); + logSyncHints(syncHints); + + for (String authority : AndroidFxAccount.getAndroidAuthorities()) { + ContentResolver.requestSync(account, authority, extras); + } + } } diff --git a/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java b/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java index 338bbf019067..fe17fc66f08f 100644 --- a/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java +++ b/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java @@ -16,7 +16,8 @@ import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClient import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.fxa.login.Engaged; import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.login.State.StateLabel; +import org.mozilla.gecko.fxa.login.State.Action; +import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper; import org.mozilla.gecko.sync.setup.activities.ActivityUtils; import android.app.Activity; @@ -41,6 +42,8 @@ public class FxAccountConfirmAccountActivity extends FxAccountAbstractActivity i // Set in onResume. protected AndroidFxAccount fxAccount; + protected final SyncStatusDelegate syncStatusDelegate = new SyncStatusDelegate(); + public FxAccountConfirmAccountActivity() { super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST); } @@ -80,11 +83,62 @@ public class FxAccountConfirmAccountActivity extends FxAccountAbstractActivity i finish(); return; } - State state = fxAccount.getState(); - if (state.getStateLabel() != StateLabel.Engaged) { - Logger.warn(LOG_TAG, "Cannot confirm Firefox Account in state: " + state.getStateLabel()); + + FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate); + + refresh(); + } + + @Override + public void onPause() { + super.onPause(); + FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate); + } + + protected class SyncStatusDelegate implements FxAccountSyncStatusHelper.Delegate { + protected final Runnable refreshRunnable = new Runnable() { + @Override + public void run() { + refresh(); + } + }; + + @Override + public AndroidFxAccount getAccount() { + return fxAccount; + } + + @Override + public void handleSyncStarted() { + Logger.info(LOG_TAG, "Got sync started message; ignoring."); + } + + @Override + public void handleSyncFinished() { + if (fxAccount == null) { + return; + } + Logger.info(LOG_TAG, "Got sync finished message; refreshing."); + runOnUiThread(refreshRunnable); + } + } + + protected void refresh() { + final State state = fxAccount.getState(); + final Action neededAction = state.getNeededAction(); + switch (neededAction) { + case NeedsVerification: + // This is what we're here to handle. + break; + case NeedsPassword: + case NeedsUpgrade: + case None: + default: + // We're not in the right place! Redirect to status. + Logger.warn(LOG_TAG, "No need to verifiy Firefox Account that needs action " + neededAction.toString() + + " (in state " + state.getStateLabel() + ")."); setResult(RESULT_CANCELED); - finish(); + this.redirectToActivity(FxAccountStatusActivity.class); return; } diff --git a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java index a51b97f84f44..b5738980f59e 100644 --- a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java +++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java @@ -11,11 +11,12 @@ import java.util.Set; import org.mozilla.gecko.R; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.preferences.PreferenceFragment; -import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.fxa.login.Married; import org.mozilla.gecko.fxa.login.State; +import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper; import org.mozilla.gecko.sync.SyncConfiguration; import android.content.ContentResolver; @@ -71,6 +72,8 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre // single account. (That is, it does not capture a single account instance.) protected Runnable requestSyncRunnable; + protected final SyncStatusDelegate syncStatusDelegate = new SyncStatusDelegate(); + protected Preference ensureFindPreference(String key) { Preference preference = findPreference(key); if (preference == null) { @@ -237,6 +240,38 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre setCheckboxesEnabled(true); } + protected class SyncStatusDelegate implements FxAccountSyncStatusHelper.Delegate { + protected final Runnable refreshRunnable = new Runnable() { + @Override + public void run() { + refresh(); + } + }; + + @Override + public AndroidFxAccount getAccount() { + return fxAccount; + } + + @Override + public void handleSyncStarted() { + if (fxAccount == null) { + return; + } + Logger.info(LOG_TAG, "Got sync started message; refreshing."); + getActivity().runOnUiThread(refreshRunnable); + } + + @Override + public void handleSyncFinished() { + if (fxAccount == null) { + return; + } + Logger.info(LOG_TAG, "Got sync finished message; refreshing."); + getActivity().runOnUiThread(refreshRunnable); + } + } + /** * Notify the fragment that a new AndroidFxAccount instance is current. *

@@ -259,9 +294,23 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre // serviced very quickly, so this is not an issue. requestSyncRunnable = new RequestSyncRunnable(); + // We would very much like register these status observers in bookended + // onResume/onPause calls, but because the Fragment gets onResume during the + // Activity's super.onResume, it hasn't yet been told its Firefox Account. + // So we register the observer here (and remove it in onPause), and open + // ourselves to the possibility that we don't have properly paired + // register/unregister calls. + FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate); + refresh(); } + @Override + public void onPause() { + super.onPause(); + FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate); + } + protected void refresh() { // refresh is called from our onResume, which can happen before the owning // Activity tells us about an account (via our public @@ -440,9 +489,7 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre return; } Logger.info(LOG_TAG, "Requesting a sync sometime soon."); - // Request a sync, but not necessarily an immediate sync. - ContentResolver.requestSync(fxAccount.getAndroidAccount(), BrowserContract.AUTHORITY, Bundle.EMPTY); - // SyncAdapter.requestImmediateSync(fxAccount.getAndroidAccount(), null); + fxAccount.requestSync(); } } @@ -459,10 +506,8 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre } else if ("debug_dump".equals(key)) { fxAccount.dump(); } else if ("debug_force_sync".equals(key)) { - Logger.info(LOG_TAG, "Syncing."); - final Bundle extras = new Bundle(); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - fxAccount.requestSync(extras); + Logger.info(LOG_TAG, "Force syncing."); + fxAccount.requestSync(FirefoxAccounts.FORCE); // No sense refreshing, since the sync will complete in the future. } else if ("debug_forget_certificate".equals(key)) { State state = fxAccount.getState(); diff --git a/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java b/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java index dee943f59dc1..2505bea160f6 100644 --- a/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java +++ b/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java @@ -16,6 +16,7 @@ import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse; import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; import org.mozilla.gecko.background.fxa.FxAccountUtils; import org.mozilla.gecko.background.fxa.PasswordStretcher; +import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; @@ -149,6 +150,7 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc return; } fxAccount.setState(new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken)); + fxAccount.requestSync(FirefoxAccounts.FORCE); // For great debugging. if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { diff --git a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java index 834a51767b51..0645eaf105a3 100644 --- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java +++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java @@ -8,12 +8,16 @@ import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.security.GeneralSecurityException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; +import java.util.List; import org.mozilla.gecko.background.common.GlobalConstants; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.fxa.FxAccountUtils; import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.login.State; import org.mozilla.gecko.fxa.login.State.StateLabel; @@ -58,6 +62,9 @@ public class AndroidFxAccount { public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel"; public static final String BUNDLE_KEY_STATE = "state"; + protected static final List ANDROID_AUTHORITIES = Collections.unmodifiableList(Arrays.asList( + new String[] { BrowserContract.AUTHORITY })); + protected final Context context; protected final AccountManager accountManager; protected final Account account; @@ -380,6 +387,10 @@ public class AndroidFxAccount { getSyncPrefs().edit().clear().commit(); } + public static Iterable getAndroidAuthorities() { + return ANDROID_AUTHORITIES; + } + /** * Return true if the underlying Android account is currently set to sync automatically. *

@@ -391,7 +402,7 @@ public class AndroidFxAccount { */ public boolean isSyncing() { boolean isSyncEnabled = true; - for (String authority : new String[] { BrowserContract.AUTHORITY }) { + for (String authority : getAndroidAuthorities()) { isSyncEnabled &= ContentResolver.getSyncAutomatically(account, authority); } return isSyncEnabled; @@ -399,7 +410,7 @@ public class AndroidFxAccount { public void enableSyncing() { Logger.info(LOG_TAG, "Enabling sync for account named like " + getObfuscatedEmail()); - for (String authority : new String[] { BrowserContract.AUTHORITY }) { + for (String authority : getAndroidAuthorities()) { ContentResolver.setSyncAutomatically(account, authority, true); ContentResolver.setIsSyncable(account, authority, 1); } @@ -407,17 +418,49 @@ public class AndroidFxAccount { public void disableSyncing() { Logger.info(LOG_TAG, "Disabling sync for account named like " + getObfuscatedEmail()); - for (String authority : new String[] { BrowserContract.AUTHORITY }) { + for (String authority : getAndroidAuthorities()) { ContentResolver.setSyncAutomatically(account, authority, false); } } - public void requestSync(Bundle extras) { - Logger.info(LOG_TAG, "Requesting sync for account named like " + getObfuscatedEmail() + - (extras.isEmpty() ? "." : "; has extras.")); - for (String authority : new String[] { BrowserContract.AUTHORITY }) { - ContentResolver.requestSync(account, authority, extras); + /** + * Is a sync currently in progress? + * + * @return true if Android is currently syncing the underlying Android Account. + */ + public boolean isCurrentlySyncing() { + boolean active = false; + for (String authority : AndroidFxAccount.getAndroidAuthorities()) { + active |= ContentResolver.isSyncActive(account, authority); } + return active; + } + + /** + * Request a sync. See {@link FirefoxAccounts#requestSync(Account, EnumSet, String[], String[])}. + */ + public void requestSync() { + requestSync(FirefoxAccounts.SOON, null, null); + } + + /** + * Request a sync. See {@link FirefoxAccounts#requestSync(Account, EnumSet, String[], String[])}. + * + * @param syncHints to pass to sync. + */ + public void requestSync(EnumSet syncHints) { + requestSync(syncHints, null, null); + } + + /** + * Request a sync. See {@link FirefoxAccounts#requestSync(Account, EnumSet, String[], String[])}. + * + * @param syncHints to pass to sync. + * @param stagesToSync stage names to sync. + * @param stagesToSkip stage names to skip. + */ + public void requestSync(EnumSet syncHints, String[] stagesToSync, String[] stagesToSkip) { + FirefoxAccounts.requestSync(getAndroidAccount(), syncHints, stagesToSync, stagesToSkip); } public synchronized void setState(State state) { diff --git a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java index 4b03215b0362..dbc7b0482ee6 100644 --- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java +++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java @@ -9,6 +9,7 @@ import java.net.URISyntaxException; import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -22,6 +23,7 @@ import org.mozilla.gecko.browserid.JSONWebTokenUtils; import org.mozilla.gecko.browserid.RSACryptoImplementation; import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient; import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate; +import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.authenticator.AccountPickler; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; @@ -64,7 +66,10 @@ import android.os.SystemClock; public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName(); - public static final int NOTIFICATION_ID = LOG_TAG.hashCode(); + public static final String SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT = "respect_local_rate_limit"; + public static final String SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF = "respect_remote_server_backoff"; + + protected static final int NOTIFICATION_ID = LOG_TAG.hashCode(); // Tracks the last seen storage hostname for backoff purposes. private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost"; @@ -423,6 +428,14 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG); Logger.resetLogging(); + Logger.info(LOG_TAG, "Syncing FxAccount" + + " account named like " + Utils.obfuscateEmail(account.name) + + " for authority " + authority + + " with instance " + this + "."); + + final EnumSet syncHints = FirefoxAccounts.getHintsToSyncFromBundle(extras); + FirefoxAccounts.logSyncHints(syncHints); + // This applies even to forced syncs, but only on success. if (this.lastSyncRealtimeMillis > 0L && (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime()) { @@ -431,11 +444,6 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { return; } - Logger.info(LOG_TAG, "Syncing FxAccount" + - " account named like " + Utils.obfuscateEmail(account.name) + - " for authority " + authority + - " with instance " + this + "."); - final Context context = getContext(); final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { @@ -480,7 +488,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { // If this sync was triggered by user action, this will be true. final boolean isImmediate = (extras != null) && (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) || - extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false)); + extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)); // If it's not an immediate sync, it must be either periodic or tickled. // Check our background rate limiter. diff --git a/mobile/android/base/fxa/sync/FxAccountSyncStatusHelper.java b/mobile/android/base/fxa/sync/FxAccountSyncStatusHelper.java new file mode 100644 index 000000000000..9d4e88e8c8e6 --- /dev/null +++ b/mobile/android/base/fxa/sync/FxAccountSyncStatusHelper.java @@ -0,0 +1,113 @@ +/* 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/. */ + +package org.mozilla.gecko.fxa.sync; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.WeakHashMap; + +import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; + +import android.content.ContentResolver; +import android.content.SyncStatusObserver; + +/** + * Abstract away some details of Android's SyncStatusObserver. + *

+ * Provides a simplified sync started/sync finished delegate. + *

+ * We would prefer to register multiple observers, but it's of limited value + * right now, so we support only a single observer, and we are as tolerant as + * possible of non-paired add/remove calls. + */ +public class FxAccountSyncStatusHelper implements SyncStatusObserver { + @SuppressWarnings("unused") + private static final String LOG_TAG = FxAccountSyncStatusHelper.class.getSimpleName(); + + protected static FxAccountSyncStatusHelper sInstance = null; + + public synchronized static FxAccountSyncStatusHelper getInstance() { + if (sInstance == null) { + sInstance = new FxAccountSyncStatusHelper(); + } + return sInstance; + } + + public interface Delegate { + public AndroidFxAccount getAccount(); + public void handleSyncStarted(); + public void handleSyncFinished(); + } + + // Used to unregister this as a listener. + protected Object handle = null; + + // Maps delegates to whether their underlying Android account was syncing the + // last time we observed a status change. + protected Map delegates = new WeakHashMap(); + + @Override + public synchronized void onStatusChanged(int which) { + for (Entry entry : delegates.entrySet()) { + final Delegate delegate = entry.getKey(); + final AndroidFxAccount fxAccount = delegate.getAccount(); + if (fxAccount == null) { + continue; + } + final boolean active = fxAccount.isCurrentlySyncing(); + // Remember for later. + boolean wasActiveLastTime = entry.getValue(); + // It's okay to update the value of an entry while iterating the entrySet. + entry.setValue(active); + + if (active && !wasActiveLastTime) { + // We've started a sync. + delegate.handleSyncStarted(); + } + if (!active && wasActiveLastTime) { + // We've finished a sync. + delegate.handleSyncFinished(); + } + } + } + + protected void addListener() { + final int mask = ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE; + if (this.handle != null) { + throw new IllegalStateException("Already registered this as an observer?"); + } + this.handle = ContentResolver.addStatusChangeListener(mask, this); + } + + protected void removeListener() { + Object handle = this.handle; + this.handle = null; + if (handle != null) { + ContentResolver.removeStatusChangeListener(handle); + } + } + + public synchronized void startObserving(Delegate delegate) { + if (delegate == null) { + throw new IllegalArgumentException("delegate must not be null"); + } + if (delegates.containsKey(delegate)) { + return; + } + // If we are the first delegate to the party, start listening. + if (delegates.isEmpty()) { + addListener(); + } + delegates.put(delegate, Boolean.FALSE); + } + + public synchronized void stopObserving(Delegate delegate) { + delegates.remove(delegate); + // If we are the last delegate leaving the party, stop listening. + if (delegates.isEmpty()) { + removeListener(); + } + } +} diff --git a/mobile/android/base/health/BrowserHealthRecorder.java b/mobile/android/base/health/BrowserHealthRecorder.java index a6bb988baecf..4311a6dfc2ea 100644 --- a/mobile/android/base/health/BrowserHealthRecorder.java +++ b/mobile/android/base/health/BrowserHealthRecorder.java @@ -190,6 +190,9 @@ public class BrowserHealthRecorder implements HealthRecorder, GeckoEventListener } private void unregisterEventListeners() { + if (state != State.INITIALIZED) { + return; + } this.dispatcher.unregisterEventListener(EVENT_SNAPSHOT, this); this.dispatcher.unregisterEventListener(EVENT_ADDONS_CHANGE, this); this.dispatcher.unregisterEventListener(EVENT_ADDONS_UNINSTALLING, this); diff --git a/mobile/android/base/home/HomeConfig.java b/mobile/android/base/home/HomeConfig.java index 3937693c4ee3..653460a41479 100644 --- a/mobile/android/base/home/HomeConfig.java +++ b/mobile/android/base/home/HomeConfig.java @@ -5,6 +5,8 @@ package org.mozilla.gecko.home; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.R; import org.mozilla.gecko.util.ThreadUtils; @@ -926,6 +928,7 @@ public final class HomeConfig { private final HomeConfig mHomeConfig; private final Map mConfigMap; private final List mConfigOrder; + private final List mEventQueue; private final Thread mOriginalThread; private PanelConfig mDefaultPanel; @@ -939,6 +942,7 @@ public final class HomeConfig { mOriginalThread = Thread.currentThread(); mConfigMap = new HashMap(); mConfigOrder = new LinkedList(); + mEventQueue = new LinkedList(); mEnabledCount = 0; mHasChanged = false; @@ -1144,6 +1148,9 @@ public final class HomeConfig { } installed = true; + + // Add an event to the queue if a new panel is sucessfully installed. + mEventQueue.add(GeckoEvent.createBroadcastEvent("HomePanels:Installed", panelConfig.getId())); } mHasChanged = true; @@ -1178,6 +1185,9 @@ public final class HomeConfig { findNewDefault(); } + // Add an event to the queue if a panel is succesfully uninstalled. + mEventQueue.add(GeckoEvent.createBroadcastEvent("HomePanels:Uninstalled", panelId)); + mHasChanged = true; return true; } @@ -1246,10 +1256,18 @@ public final class HomeConfig { final State newConfigState = new State(mHomeConfig, makeOrderedCopy(true), isDefault()); + // Copy the event queue to a new list, so that we only modify mEventQueue on + // the original thread where it was created. + final LinkedList eventQueueCopy = new LinkedList(mEventQueue); + mEventQueue.clear(); + ThreadUtils.getBackgroundHandler().post(new Runnable() { @Override public void run() { mHomeConfig.save(newConfigState); + + // Send pending events after the new config is saved. + sendEventsToGecko(eventQueueCopy); } }); @@ -1272,6 +1290,10 @@ public final class HomeConfig { // need to deep copy the current PanelConfig instances. mHomeConfig.save(newConfigState); + // Send pending events after the new config is saved. + sendEventsToGecko(mEventQueue); + mEventQueue.clear(); + return newConfigState; } @@ -1289,6 +1311,12 @@ public final class HomeConfig { return mConfigMap.isEmpty(); } + private void sendEventsToGecko(List events) { + for (GeckoEvent e : events) { + GeckoAppShell.sendEventToGecko(e); + } + } + private class EditorIterator implements Iterator { private final Iterator mOrderIterator; diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 347e65231007..91bd12624798 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -60,6 +60,7 @@ gujar.sources += [ 'util/INISection.java', 'util/JSONUtils.java', 'util/MenuUtils.java', + 'util/NativeEventListener.java', 'util/NativeJSContainer.java', 'util/NativeJSObject.java', 'util/NonEvictingLruCache.java', diff --git a/mobile/android/base/sync/setup/activities/SendTabActivity.java b/mobile/android/base/sync/setup/activities/SendTabActivity.java index bd0b36952d73..05d9cd9e6926 100644 --- a/mobile/android/base/sync/setup/activities/SendTabActivity.java +++ b/mobile/android/base/sync/setup/activities/SendTabActivity.java @@ -12,6 +12,7 @@ import java.util.Map.Entry; import org.mozilla.gecko.R; import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.fxa.FirefoxAccounts; import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity; import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity; @@ -22,7 +23,6 @@ import org.mozilla.gecko.sync.CommandRunner; import org.mozilla.gecko.sync.GlobalSession; import org.mozilla.gecko.sync.SyncConfiguration; import org.mozilla.gecko.sync.SyncConstants; -import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.sync.repositories.NullCursorException; import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; import org.mozilla.gecko.sync.repositories.domain.ClientRecord; @@ -33,7 +33,6 @@ import org.mozilla.gecko.sync.syncadapter.SyncAdapter; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Activity; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -80,10 +79,7 @@ public class SendTabActivity extends Activity { @Override public void syncClientsStage() { - final Bundle extras = new Bundle(); - Utils.putStageNamesToSync(extras, CLIENTS_STAGE, null); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - this.fxAccount.requestSync(extras); + fxAccount.requestSync(FirefoxAccounts.FORCE, CLIENTS_STAGE, null); } } diff --git a/mobile/android/base/tests/BaseTest.java b/mobile/android/base/tests/BaseTest.java index 61e1a398e3f6..444ff3c40c38 100644 --- a/mobile/android/base/tests/BaseTest.java +++ b/mobile/android/base/tests/BaseTest.java @@ -20,6 +20,7 @@ import org.mozilla.gecko.Element; import org.mozilla.gecko.FennecNativeActions; import org.mozilla.gecko.FennecNativeDriver; import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoThread; import org.mozilla.gecko.GeckoThread.LaunchState; import org.mozilla.gecko.R; @@ -137,6 +138,10 @@ abstract class BaseTest extends BaseRobocopTest { public void tearDown() throws Exception { try { mAsserter.endTest(); + // request a force quit of the browser and wait for it to take effect + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null)); + mSolo.sleep(7000); + // if still running, finish activities as recommended by Robotium mSolo.finishOpenedActivities(); } catch (Throwable e) { e.printStackTrace(); diff --git a/mobile/android/base/tests/UITest.java b/mobile/android/base/tests/UITest.java index cd5476cd1c26..b3e151cf2d15 100644 --- a/mobile/android/base/tests/UITest.java +++ b/mobile/android/base/tests/UITest.java @@ -11,6 +11,8 @@ import org.mozilla.gecko.Assert; import org.mozilla.gecko.Driver; import org.mozilla.gecko.FennecNativeActions; import org.mozilla.gecko.FennecNativeDriver; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.tests.components.AboutHomeComponent; import org.mozilla.gecko.tests.components.AppMenuComponent; import org.mozilla.gecko.tests.components.BaseComponent; @@ -78,6 +80,10 @@ abstract class UITest extends BaseRobocopTest public void tearDown() throws Exception { try { mAsserter.endTest(); + // request a force quit of the browser and wait for it to take effect + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null)); + mSolo.sleep(7000); + // if still running, finish activities as recommended by Robotium mSolo.finishOpenedActivities(); } catch (Throwable e) { e.printStackTrace(); diff --git a/mobile/android/base/tests/robocop_testharness.js b/mobile/android/base/tests/robocop_testharness.js index 00bae7e1e60b..94444f03b333 100644 --- a/mobile/android/base/tests/robocop_testharness.js +++ b/mobile/android/base/tests/robocop_testharness.js @@ -3,12 +3,8 @@ * 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/. */ -let bridge = SpecialPowers.Cc["@mozilla.org/android/bridge;1"] - .getService(SpecialPowers.Ci.nsIAndroidBridge); - function sendMessageToJava(message) { - let data = JSON.stringify(message); - bridge.handleGeckoMessage(data); + SpecialPowers.Services.androidBridge.handleGeckoMessage(message); } function _evalURI(uri, sandbox) { diff --git a/mobile/android/base/util/NativeEventListener.java b/mobile/android/base/util/NativeEventListener.java new file mode 100644 index 000000000000..90deeedf1e0b --- /dev/null +++ b/mobile/android/base/util/NativeEventListener.java @@ -0,0 +1,13 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.util; + +import org.mozilla.gecko.mozglue.RobocopTarget; + +@RobocopTarget +public interface NativeEventListener { + void handleMessage(String event, NativeJSObject message); +} diff --git a/mobile/android/chrome/content/about.xhtml b/mobile/android/chrome/content/about.xhtml index 035b1a7191f5..89975eb9c7dd 100644 --- a/mobile/android/chrome/content/about.xhtml +++ b/mobile/android/chrome/content/about.xhtml @@ -145,17 +145,17 @@ function checkForUpdates() { showCheckingMessage(); - Services.androidBridge.handleGeckoMessage(JSON.stringify({ type: "Update:Check" })); + Services.androidBridge.handleGeckoMessage({ type: "Update:Check" }); } function downloadUpdate() { - Services.androidBridge.handleGeckoMessage(JSON.stringify({ type: "Update:Download" })); + Services.androidBridge.handleGeckoMessage({ type: "Update:Download" }); } function installUpdate() { showCheckAction(); - Services.androidBridge.handleGeckoMessage(JSON.stringify({ type: "Update:Install" })); + Services.androidBridge.handleGeckoMessage({ type: "Update:Install" }); } let updateLink = document.getElementById("updateLink"); diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 0a6007ee054d..7757bd0e70a5 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -131,7 +131,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", // Lazily-loaded JS modules that use observer notifications [ - ["Home", ["HomePanels:Get", "HomePanels:Authenticate"], "resource://gre/modules/Home.jsm"], + ["Home", ["HomePanels:Get", "HomePanels:Authenticate", + "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"], ].forEach(module => { let [name, notifications, resource] = module; XPCOMUtils.defineLazyModuleGetter(this, name, resource); diff --git a/mobile/android/components/FilePicker.js b/mobile/android/components/FilePicker.js index 0a5758c1e5d2..783546e92a45 100644 --- a/mobile/android/components/FilePicker.js +++ b/mobile/android/components/FilePicker.js @@ -225,7 +225,7 @@ FilePicker.prototype = { }, sendMessageToJava: function(aMsg) { - Services.androidBridge.handleGeckoMessage(JSON.stringify(aMsg)); + Services.androidBridge.handleGeckoMessage(aMsg); }, observe: function(aSubject, aTopic, aData) { diff --git a/mobile/android/modules/ContactService.jsm b/mobile/android/modules/ContactService.jsm index aa00175a7269..862b2d841794 100644 --- a/mobile/android/modules/ContactService.jsm +++ b/mobile/android/modules/ContactService.jsm @@ -47,7 +47,7 @@ let ContactService = { }, _sendMessageToJava: function(aMsg) { - Services.androidBridge.handleGeckoMessage(JSON.stringify(aMsg)); + Services.androidBridge.handleGeckoMessage(aMsg); }, _sendReturnMessage: function(aTopic, aRequestID, aResult) { diff --git a/mobile/android/modules/Home.jsm b/mobile/android/modules/Home.jsm index 272730a7a379..9e8c183cab82 100644 --- a/mobile/android/modules/Home.jsm +++ b/mobile/android/modules/Home.jsm @@ -157,12 +157,74 @@ let HomeBanner = (function () { }); })(); -// We need this function to have access to the HomePanels +// We need this object to have access to the HomePanels // private members without leaking it outside Home.jsm. -let handlePanelsGet; -let handlePanelsAuthenticate; +let HomePanelsMessageHandlers; let HomePanels = (function () { + // Functions used to handle messages sent from Java. + HomePanelsMessageHandlers = { + + "HomePanels:Get": function handlePanelsGet(data) { + data = JSON.parse(data); + + let requestId = data.requestId; + let ids = data.ids || null; + + let panels = []; + for (let id in _registeredPanels) { + // Null ids means we want to fetch all available panels + if (ids == null || ids.indexOf(id) >= 0) { + try { + panels.push(_generatePanel(id)); + } catch(e) { + Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e); + } + } + } + + sendMessageToJava({ + type: "HomePanels:Data", + panels: panels, + requestId: requestId + }); + }, + + "HomePanels:Authenticate": function handlePanelsAuthenticate(id) { + // Generate panel options to get auth handler. + let options = _registeredPanels[id](); + if (!options.auth) { + throw "Home.panels: Invalid auth for panel.id = " + id; + } + if (!options.auth.authenticate || typeof options.auth.authenticate !== "function") { + throw "Home.panels: Invalid auth authenticate function: panel.id = " + this.id; + } + options.auth.authenticate(); + }, + + "HomePanels:Installed": function handlePanelsInstalled(id) { + let options = _registeredPanels[id](); + if (!options.oninstall) { + return; + } + if (typeof options.oninstall !== "function") { + throw "Home.panels: Invalid oninstall function: panel.id = " + this.id; + } + options.oninstall(); + }, + + "HomePanels:Uninstalled": function handlePanelsUninstalled(id) { + let options = _registeredPanels[id](); + if (!options.onuninstall) { + return; + } + if (typeof options.onuninstall !== "function") { + throw "Home.panels: Invalid onuninstall function: panel.id = " + this.id; + } + options.onuninstall(); + } + }; + // Holds the current set of registered panels that can be // installed, updated, uninstalled, or unregistered. It maps // panel ids with the functions that dynamically generate @@ -240,22 +302,22 @@ let HomePanels = (function () { } } - if (options.authHandler) { - if (!options.authHandler.messageText) { - throw "Home.panels: Invalid authHandler messageText: panel.id = " + this.id; + if (options.auth) { + if (!options.auth.messageText) { + throw "Home.panels: Invalid auth messageText: panel.id = " + this.id; } - if (!options.authHandler.buttonText) { - throw "Home.panels: Invalid authHandler buttonText: panel.id = " + this.id; + if (!options.auth.buttonText) { + throw "Home.panels: Invalid auth buttonText: panel.id = " + this.id; } this.authConfig = { - messageText: options.authHandler.messageText, - buttonText: options.authHandler.buttonText + messageText: options.auth.messageText, + buttonText: options.auth.buttonText }; // Include optional image URL if it is specified. - if (options.authHandler.imageUrl) { - this.authConfig.imageUrl = options.authHandler.imageUrl; + if (options.auth.imageUrl) { + this.authConfig.imageUrl = options.auth.imageUrl; } } } @@ -265,41 +327,6 @@ let HomePanels = (function () { return new Panel(id, options); }; - handlePanelsGet = function(data) { - let requestId = data.requestId; - let ids = data.ids || null; - - let panels = []; - for (let id in _registeredPanels) { - // Null ids means we want to fetch all available panels - if (ids == null || ids.indexOf(id) >= 0) { - try { - panels.push(_generatePanel(id)); - } catch(e) { - Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e); - } - } - } - - sendMessageToJava({ - type: "HomePanels:Data", - panels: panels, - requestId: requestId - }); - }; - - handlePanelsAuthenticate = function(id) { - // Generate panel options to get auth handler. - let options = _registeredPanels[id](); - if (!options.authHandler) { - throw "Home.panels: Invalid authHandler for panel.id = " + id; - } - if (!options.authHandler.authenticate || typeof options.authHandler.authenticate !== "function") { - throw "Home.panels: Invalid authHandler authenticate function: panel.id = " + this.id; - } - options.authHandler.authenticate(); - }; - // Helper function used to see if a value is in an object. let _valueExists = function(obj, value) { for (let key in obj) { @@ -385,13 +412,10 @@ this.Home = Object.freeze({ // Lazy notification observer registered in browser.js observe: function(subject, topic, data) { - switch(topic) { - case "HomePanels:Get": - handlePanelsGet(JSON.parse(data)); - break; - case "HomePanels:Authenticate": - handlePanelsAuthenticate(data); - break; + if (topic in HomePanelsMessageHandlers) { + HomePanelsMessageHandlers[topic](data); + } else { + Cu.reportError("Home.observe: message handler not found for topic: " + topic); } } }); diff --git a/mobile/android/modules/LightweightThemeConsumer.jsm b/mobile/android/modules/LightweightThemeConsumer.jsm index e4d6411161c9..c939859fa42d 100644 --- a/mobile/android/modules/LightweightThemeConsumer.jsm +++ b/mobile/android/modules/LightweightThemeConsumer.jsm @@ -39,6 +39,6 @@ LightweightThemeConsumer.prototype = { let msg = active ? { type: "LightweightTheme:Update", data: aData } : { type: "LightweightTheme:Disable" }; - Services.androidBridge.handleGeckoMessage(JSON.stringify(msg)); + Services.androidBridge.handleGeckoMessage(msg); } } diff --git a/mobile/android/modules/Messaging.jsm b/mobile/android/modules/Messaging.jsm index 703468212208..8086831e2505 100644 --- a/mobile/android/modules/Messaging.jsm +++ b/mobile/android/modules/Messaging.jsm @@ -37,5 +37,5 @@ function sendMessageToJava(aMessage, aCallback) { Services.obs.addObserver(obs, aMessage.type + ":Error", false); } - return Services.androidBridge.handleGeckoMessage(JSON.stringify(aMessage)); + return Services.androidBridge.handleGeckoMessage(aMessage); } diff --git a/mobile/android/modules/Notifications.jsm b/mobile/android/modules/Notifications.jsm index 17a287c1d751..d18166e8ee59 100644 --- a/mobile/android/modules/Notifications.jsm +++ b/mobile/android/modules/Notifications.jsm @@ -130,7 +130,7 @@ Notification.prototype = { if (this._light) msg.light = this._light; - Services.androidBridge.handleGeckoMessage(JSON.stringify(msg)); + Services.androidBridge.handleGeckoMessage(msg); return this; }, @@ -139,7 +139,7 @@ Notification.prototype = { type: "Notification:Hide", id: this._id }; - Services.androidBridge.handleGeckoMessage(JSON.stringify(msg)); + Services.androidBridge.handleGeckoMessage(msg); } } diff --git a/mobile/android/tests/background/junit3/android-services-files.mk b/mobile/android/tests/background/junit3/android-services-files.mk index 466c8707c9d7..095db1308032 100644 --- a/mobile/android/tests/background/junit3/android-services-files.mk +++ b/mobile/android/tests/background/junit3/android-services-files.mk @@ -24,6 +24,7 @@ BACKGROUND_TESTS_JAVA_FILES := \ src/db/TestPasswordsRepository.java \ src/fxa/authenticator/TestAccountPickler.java \ src/fxa/TestBrowserIDKeyPairGeneration.java \ + src/fxa/TestFirefoxAccounts.java \ src/healthreport/MockDatabaseEnvironment.java \ src/healthreport/MockHealthReportDatabaseStorage.java \ src/healthreport/MockHealthReportSQLiteOpenHelper.java \ diff --git a/mobile/android/tests/background/junit3/src/fxa/TestFirefoxAccounts.java b/mobile/android/tests/background/junit3/src/fxa/TestFirefoxAccounts.java new file mode 100644 index 000000000000..dedee920f77a --- /dev/null +++ b/mobile/android/tests/background/junit3/src/fxa/TestFirefoxAccounts.java @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.fxa; + +import java.util.EnumSet; + +import junit.framework.Assert; + +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.fxa.FirefoxAccounts; +import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.setup.Constants; + +import android.content.ContentResolver; +import android.os.Bundle; + +public class TestFirefoxAccounts extends AndroidSyncTestCase { + private static class InnerFirefoxAccounts extends FirefoxAccounts { + // For testing, since putHintsToSync is not public. + public static Bundle makeTestBundle(EnumSet syncHints, String[] stagesToSync, String[] stagesToSkip) { + final Bundle bundle = new Bundle(); + FirefoxAccounts.putHintsToSync(bundle, syncHints); + Utils.putStageNamesToSync(bundle, stagesToSync, stagesToSkip); + return bundle; + } + } + + protected void assertBundle(Bundle bundle, boolean manual, boolean respectLocal, boolean respectRemote, String stagesToSync, String stagesToSkip) { + Assert.assertEquals(manual, bundle.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL)); + Assert.assertEquals(respectLocal, bundle.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_LOCAL_RATE_LIMIT)); + Assert.assertEquals(respectRemote, bundle.getBoolean(FxAccountSyncAdapter.SYNC_EXTRAS_RESPECT_REMOTE_SERVER_BACKOFF)); + Assert.assertEquals(stagesToSync, bundle.getString(Constants.EXTRAS_KEY_STAGES_TO_SYNC)); + Assert.assertEquals(stagesToSkip, bundle.getString(Constants.EXTRAS_KEY_STAGES_TO_SKIP)); + } + + public void testMakeTestBundle() { + Bundle bundle; + bundle = InnerFirefoxAccounts.makeTestBundle(FirefoxAccounts.FORCE, new String[] { "clients" }, null); + assertBundle(bundle, true, false, false, "{\"clients\":0}", null); + assertEquals(FirefoxAccounts.FORCE, FirefoxAccounts.getHintsToSyncFromBundle(bundle)); + + bundle = InnerFirefoxAccounts.makeTestBundle(FirefoxAccounts.NOW, null, new String[] { "bookmarks" }); + assertBundle(bundle, true, true, true, null, "{\"bookmarks\":0}"); + assertEquals(FirefoxAccounts.NOW, FirefoxAccounts.getHintsToSyncFromBundle(bundle)); + + bundle = InnerFirefoxAccounts.makeTestBundle(FirefoxAccounts.SOON, null, null); + assertBundle(bundle, false, true, true, null, null); + assertEquals(FirefoxAccounts.SOON, FirefoxAccounts.getHintsToSyncFromBundle(bundle)); + } +} diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index 17f663dc2733..b974982ccc10 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -4239,7 +4239,7 @@ pref("memory.blob_report.stack_frames", 0); pref("social.whitelist", "https://mozsocial.cliqz.com,https://now.msn.com,https://mixi.jp"); // comma separated list of domain origins (e.g. https://domain.com) for // directory websites (e.g. AMO) that can install providers for other sites -pref("social.directories", "https://activations.mozilla.org"); +pref("social.directories", "https://activations.cdn.mozilla.net"); // remote-install allows any website to activate a provider, with extended UI // notifying user of installation. we can later pref off remote install if // necessary. This does not affect whitelisted and directory installs. diff --git a/netwerk/base/src/ProxyAutoConfig.cpp b/netwerk/base/src/ProxyAutoConfig.cpp index f05d8e6cec87..d01733217989 100644 --- a/netwerk/base/src/ProxyAutoConfig.cpp +++ b/netwerk/base/src/ProxyAutoConfig.cpp @@ -585,7 +585,9 @@ const JSClass JSRuntimeWrapper::sGlobalClass = { "PACResolutionThreadGlobal", JSCLASS_GLOBAL_FLAGS, JS_PropertyStub, JS_DeletePropertyStub, JS_PropertyStub, JS_StrictPropertyStub, - JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub + JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, + nullptr, nullptr, nullptr, nullptr, + JS_GlobalObjectTraceHook }; nsresult diff --git a/testing/mochitest/chrome/test_sample.xul b/testing/mochitest/chrome/test_sample.xul index 08593bba263d..d964572b1e1d 100644 --- a/testing/mochitest/chrome/test_sample.xul +++ b/testing/mochitest/chrome/test_sample.xul @@ -6,10 +6,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=8675309 --> - Test for Bug 8675309 diff --git a/toolkit/content/tests/chrome/test_bug263683.xul b/toolkit/content/tests/chrome/test_bug263683.xul index 1ed85e484f66..c5755c9f176d 100644 --- a/toolkit/content/tests/chrome/test_bug263683.xul +++ b/toolkit/content/tests/chrome/test_bug263683.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=263683 --> - Test for Bug 263683 diff --git a/toolkit/content/tests/chrome/test_bug304188.xul b/toolkit/content/tests/chrome/test_bug304188.xul index 837abfe5f34d..f41d24f9bbc3 100644 --- a/toolkit/content/tests/chrome/test_bug304188.xul +++ b/toolkit/content/tests/chrome/test_bug304188.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=304188 --> - Test for Bug 304188 diff --git a/toolkit/content/tests/chrome/test_bug331215.xul b/toolkit/content/tests/chrome/test_bug331215.xul index f4b1a991e166..e0d0d1e0a945 100644 --- a/toolkit/content/tests/chrome/test_bug331215.xul +++ b/toolkit/content/tests/chrome/test_bug331215.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=331215 --> - Test for Bug 331215 diff --git a/toolkit/content/tests/chrome/test_bug360220.xul b/toolkit/content/tests/chrome/test_bug360220.xul index cb711519e67e..69923ef29008 100644 --- a/toolkit/content/tests/chrome/test_bug360220.xul +++ b/toolkit/content/tests/chrome/test_bug360220.xul @@ -5,10 +5,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=360220 --> - Test for Bug 360220 diff --git a/toolkit/content/tests/chrome/test_bug360437.xul b/toolkit/content/tests/chrome/test_bug360437.xul index 8c003cc5a7fd..eb17adcf5dc4 100644 --- a/toolkit/content/tests/chrome/test_bug360437.xul +++ b/toolkit/content/tests/chrome/test_bug360437.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=360437 --> - Test for Bug 360437 diff --git a/toolkit/content/tests/chrome/test_bug365773.xul b/toolkit/content/tests/chrome/test_bug365773.xul index a55b088a6d5d..17385365ad7b 100644 --- a/toolkit/content/tests/chrome/test_bug365773.xul +++ b/toolkit/content/tests/chrome/test_bug365773.xul @@ -5,10 +5,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=365773 --> - Test for Bug 365773 diff --git a/toolkit/content/tests/chrome/test_bug366992.xul b/toolkit/content/tests/chrome/test_bug366992.xul index b63af265115c..2c92defc5484 100644 --- a/toolkit/content/tests/chrome/test_bug366992.xul +++ b/toolkit/content/tests/chrome/test_bug366992.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=366992 --> - Test for Bug 366992 diff --git a/toolkit/content/tests/chrome/test_bug409624.xul b/toolkit/content/tests/chrome/test_bug409624.xul index 1ae08306bb26..59a862cad75b 100644 --- a/toolkit/content/tests/chrome/test_bug409624.xul +++ b/toolkit/content/tests/chrome/test_bug409624.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=409624 --> - Test for Bug 409624 diff --git a/toolkit/content/tests/chrome/test_bug429723.xul b/toolkit/content/tests/chrome/test_bug429723.xul index 9382261fa921..99ee8acd9780 100644 --- a/toolkit/content/tests/chrome/test_bug429723.xul +++ b/toolkit/content/tests/chrome/test_bug429723.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=429723 --> - Test for Bug 429723 diff --git a/toolkit/content/tests/chrome/test_bug437844.xul b/toolkit/content/tests/chrome/test_bug437844.xul index c7c65b9d0c2f..81b3f1be7fdf 100644 --- a/toolkit/content/tests/chrome/test_bug437844.xul +++ b/toolkit/content/tests/chrome/test_bug437844.xul @@ -8,10 +8,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=437844 https://bugzilla.mozilla.org/show_bug.cgi?id=348233 --> - Test for Bug 437844 and Bug 348233 diff --git a/toolkit/content/tests/chrome/test_bug570192.xul b/toolkit/content/tests/chrome/test_bug570192.xul index 2b19280a112f..09f73e932b5d 100644 --- a/toolkit/content/tests/chrome/test_bug570192.xul +++ b/toolkit/content/tests/chrome/test_bug570192.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=570192 --> - Test for Bug 570192 diff --git a/toolkit/content/tests/chrome/test_colorpicker_popup.xul b/toolkit/content/tests/chrome/test_colorpicker_popup.xul index 426f5a40672a..3ac84260b2b8 100644 --- a/toolkit/content/tests/chrome/test_colorpicker_popup.xul +++ b/toolkit/content/tests/chrome/test_colorpicker_popup.xul @@ -6,10 +6,8 @@ onload="setTimeout(runTests, 0);" onpopupshown="popupShown();" onpopuphidden="popupHiding();" - xmlns:html="http://www.w3.org/1999/xhtml" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - Colorpicker Tests diff --git a/toolkit/content/tests/chrome/test_findbar.xul b/toolkit/content/tests/chrome/test_findbar.xul index bc5df9e492b8..3d89b8b041e6 100644 --- a/toolkit/content/tests/chrome/test_findbar.xul +++ b/toolkit/content/tests/chrome/test_findbar.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=288254 --> - Test for Bug 288254 diff --git a/toolkit/content/tests/chrome/test_findbar_events.xul b/toolkit/content/tests/chrome/test_findbar_events.xul index f70c76cb25a7..d75e5ccb57e8 100644 --- a/toolkit/content/tests/chrome/test_findbar_events.xul +++ b/toolkit/content/tests/chrome/test_findbar_events.xul @@ -7,10 +7,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=793275 --> - Test for Bug 793275 diff --git a/toolkit/content/tests/chrome/test_largemenu.xul b/toolkit/content/tests/chrome/test_largemenu.xul index fd03c59c94db..8841e2b16a68 100644 --- a/toolkit/content/tests/chrome/test_largemenu.xul +++ b/toolkit/content/tests/chrome/test_largemenu.xul @@ -4,10 +4,8 @@ - Large Menu Tests diff --git a/toolkit/content/tests/chrome/test_menulist_null_value.xul b/toolkit/content/tests/chrome/test_menulist_null_value.xul index 7e41af1198aa..2545b6cdecad 100644 --- a/toolkit/content/tests/chrome/test_menulist_null_value.xul +++ b/toolkit/content/tests/chrome/test_menulist_null_value.xul @@ -4,10 +4,8 @@ - Menulist value property diff --git a/toolkit/content/tests/chrome/test_panel_focus.xul b/toolkit/content/tests/chrome/test_panel_focus.xul index 826ae4ac9a38..e18f28ca8f6b 100644 --- a/toolkit/content/tests/chrome/test_panel_focus.xul +++ b/toolkit/content/tests/chrome/test_panel_focus.xul @@ -4,10 +4,8 @@ - Panel Focus Tests diff --git a/toolkit/content/tests/chrome/test_popup_moveToAnchor.xul b/toolkit/content/tests/chrome/test_popup_moveToAnchor.xul index bd0c5811205f..344002fdf65e 100644 --- a/toolkit/content/tests/chrome/test_popup_moveToAnchor.xul +++ b/toolkit/content/tests/chrome/test_popup_moveToAnchor.xul @@ -4,13 +4,12 @@ - Popup moveToAnchor Tests -