merge autoland to mozilla-central. r=merge a=merge

MozReview-Commit-ID: 9SALJlvWgoZ
This commit is contained in:
Sebastian Hengst 2017-09-13 13:32:44 +02:00
commit e4a2f44531
423 changed files with 5875 additions and 2743 deletions

View File

@ -35,3 +35,13 @@ BinPackArguments: false
BinPackParameters: false
SpaceAfterTemplateKeyword: false
ReflowComments: false
BreakBeforeBraces: Custom
BraceWrapping:
AfterEnum: true
AfterStruct: true
AfterFunction: true
AfterClass: true
SplitEmptyFunction: true

View File

@ -368,6 +368,10 @@ pref("browser.download.animateNotifications", true);
// This records whether or not the panel has been shown at least once.
pref("browser.download.panel.shown", false);
// This controls whether the button is automatically shown/hidden depending
// on whether there are downloads to show.
pref("browser.download.autohideButton", true);
#ifndef XP_MACOSX
pref("browser.helperApps.deleteTempFileOnExit", true);
#endif
@ -1266,6 +1270,7 @@ pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/
// activates Activity Stream
pref("browser.newtabpage.activity-stream.enabled", true);
pref("browser.newtabpage.activity-stream.prerender", true);
pref("browser.newtabpage.activity-stream.aboutHome.enabled", false);
// Enable the DOM fullscreen API.
@ -1716,6 +1721,7 @@ pref("browser.suppress_first_window_animation", true);
pref("browser.onboarding.enabled", true);
// Mark this as an upgraded profile so we don't offer the initial new user onboarding tour.
pref("browser.onboarding.tourset-version", 2);
pref("browser.onboarding.state", "default");
// On the Activity-Stream page, the snippet's position overlaps with our notification.
// So use `browser.onboarding.notification.finished` to let the AS page know
// if our notification is finished and safe to show their snippet.

View File

@ -39,7 +39,6 @@ var CustomizationHandler = {
UpdateUrlbarSearchSplitterState();
PlacesToolbarHelper.customizeStart();
DownloadsButton.customizeStart();
},
_customizationEnding(aDetails) {
@ -63,7 +62,6 @@ var CustomizationHandler = {
}
PlacesToolbarHelper.customizeDone();
DownloadsButton.customizeDone();
UpdateUrlbarSearchSplitterState();

View File

@ -95,6 +95,11 @@ panelview:not([mainview]):not([current]) {
visibility: collapse;
}
photonpanelmultiview panelview:not([current]) {
transition: none;
visibility: collapse;
}
panelview[mainview] > .panel-header,
panelview:not([title]) > .panel-header {
display: none;
@ -518,10 +523,9 @@ toolbar:not(#TabsToolbar) > #personal-bookmarks {
/* ::::: location bar & search bar ::::: */
#urlbar-container {
min-width: 50ch;
}
/* url bar min-width is defined further down, together with the maximum size
* of the identity icon block, for different window sizes.
*/
#search-container {
min-width: 25ch;
}
@ -701,41 +705,69 @@ html|input.urlbar-input[textoverflow]:not([focused]) {
-moz-user-focus: ignore;
}
/* We leave 49ch plus whatever space the download button will need when it
* appears. Normally this should be 16px for the icon, plus 2 * 2px padding
* plus the toolbarbutton-inner-padding. We're adding 4px to ensure things
* like rounding on hidpi don't accidentally result in the button going
* into overflow.
*/
#urlbar-container {
min-width: calc(49ch + 24px + 2 * var(--toolbarbutton-inner-padding));
}
#nav-bar[downloadsbuttonshown] #urlbar-container {
min-width: 49ch;
}
#identity-icon-labels {
max-width: 18em;
max-width: 17em;
}
@media (max-width: 700px) {
#urlbar-container {
min-width: 45ch;
min-width: calc(44ch + 24px + 2 * var(--toolbarbutton-inner-padding));
}
#identity-icon-labels {
max-width: 70px;
}
}
@media (max-width: 600px) {
#urlbar-container {
min-width: 40ch;
#nav-bar[downloadsbuttonshown] #urlbar-container {
min-width: 44ch;
}
#identity-icon-labels {
max-width: 60px;
}
}
@media (max-width: 500px) {
@media (max-width: 600px) {
#urlbar-container {
min-width: 35ch;
min-width: calc(39ch + 24px + 2 * var(--toolbarbutton-inner-padding));
}
#nav-bar[downloadsbuttonshown] #urlbar-container {
min-width: 39ch;
}
#identity-icon-labels {
max-width: 50px;
}
}
@media (max-width: 400px) {
@media (max-width: 500px) {
#urlbar-container {
min-width: 28ch;
min-width: calc(34ch + 24px + 2 * var(--toolbarbutton-inner-padding));
}
#nav-bar[downloadsbuttonshown] #urlbar-container {
min-width: 34ch;
}
#identity-icon-labels {
max-width: 40px;
}
}
@media (max-width: 400px) {
#urlbar-container {
min-width: calc(27ch + 24px + 2 * var(--toolbarbutton-inner-padding));
}
#nav-bar[downloadsbuttonshown] #urlbar-container {
min-width: 27ch;
}
#identity-icon-labels {
max-width: 30px;
}
}
#identity-icon-country-label {
direction: ltr;

View File

@ -1321,6 +1321,10 @@ var gBrowserInit = {
SidebarUI.init();
// We do this in onload because we want to ensure the button's state
// doesn't flicker as the window is being shown.
DownloadsButton.init();
// Certain kinds of automigration rely on this notification to complete
// their tasks BEFORE the browser window is shown. SessionStore uses it to
// restore tabs into windows AFTER important parts like gMultiProcessBrowser
@ -1847,6 +1851,8 @@ var gBrowserInit = {
SidebarUI.uninit();
DownloadsButton.uninit();
// Now either cancel delayedStartup, or clean up the services initialized from
// it.
if (this._boundDelayedStartup) {

View File

@ -939,7 +939,9 @@
ondragenter="DownloadsIndicatorView.onDragOver(event);"
label="&downloads.label;"
removable="true"
overflows="false"
cui-areatype="toolbar"
hidden="true"
tooltip="dynamic-shortcut-tooltip"/>
<toolbarbutton id="library-button" class="toolbarbutton-1 chromeclass-toolbar-additional"

View File

@ -74,7 +74,7 @@ async function expectFocusOnF6(backward, expectedDocument, expectedElement, onCo
}
is(fm.focusedWindow.document.documentElement.id, expectedDocument, desc + " document matches");
is(fm.focusedElement, expectedElement, desc + " element matches");
is(fm.focusedElement, expectedElement, desc + " element matches (wanted: " + expectedElement.id + " got: " + fm.focusedElement.id + ")");
if (onContent) {
window.messageManager.removeMessageListener("BrowserTest:FocusChanged", focusChangedListener);
@ -171,8 +171,9 @@ add_task(async function() {
});
// Navigate when the downloads panel is open
add_task(async function() {
await pushPrefs(["accessibility.tabfocus", 7]);
add_task(async function test_download_focus() {
await pushPrefs(["accessibility.tabfocus", 7], ["browser.download.autohideButton", false]);
await promiseButtonShown("downloads-button");
let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown", true);
EventUtils.synthesizeMouseAtCenter(document.getElementById("downloads-button"), { });
@ -253,3 +254,12 @@ add_task(async function() {
});
// XXXndeakin add tests for browsers inside of panels
function promiseButtonShown(id) {
let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
return BrowserTestUtils.waitForCondition(() => {
let target = document.getElementById(id);
let bounds = dwu.getBoundsWithoutFlushing(target);
return bounds.width > 0 && bounds.height > 0;
}, `Waiting for button ${id} to have non-0 size`);
}

View File

@ -68,7 +68,7 @@ const EXPECTED_APPMENU_SUBVIEW_REFLOWS = [
{
stack: [
"descriptionHeightWorkaround@resource:///modules/PanelMultiView.jsm",
"onTransitionEnd@resource:///modules/PanelMultiView.jsm",
"_cleanupTransitionPhase@resource:///modules/PanelMultiView.jsm",
],
times: 2, // This number should only ever go down - never up.

View File

@ -87,10 +87,6 @@ var whitelist = [
{file: "resource://app/modules/NewTabSearchProvider.jsm"},
{file: "resource://app/modules/NewTabWebChannel.jsm"},
// browser/extensions/activity-stream/data/content/activity-stream-prerendered.html
// This will used when Bug 1397875 lands
{file: "resource://activity-stream/data/content/activity-stream-prerendered.html"},
// layout/mathml/nsMathMLChar.cpp
{file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties"},
{file: "resource://gre/res/fonts/mathfontUnicode.properties"},

View File

@ -9,8 +9,8 @@ const DRAG_WORD = "Firefox";
add_task(async function checkDragURL() {
await BrowserTestUtils.withNewTab(TEST_URL, function(browser) {
// Have to use something other than the URL bar as a source, so picking the
// downloads button somewhat arbitrarily:
EventUtils.synthesizeDrop(document.getElementById("downloads-button"), gURLBar,
// home button somewhat arbitrarily:
EventUtils.synthesizeDrop(document.getElementById("home-button"), gURLBar,
[[{type: "text/plain", data: DRAG_URL}]], "copy", window);
is(gURLBar.value, TEST_URL, "URL bar value should not have changed");
is(gBrowser.selectedBrowser.userTypedValue, null, "Stored URL bar value should not have changed");
@ -19,7 +19,7 @@ add_task(async function checkDragURL() {
add_task(async function checkDragForbiddenURL() {
await BrowserTestUtils.withNewTab(TEST_URL, function(browser) {
EventUtils.synthesizeDrop(document.getElementById("downloads-button"), gURLBar,
EventUtils.synthesizeDrop(document.getElementById("home-button"), gURLBar,
[[{type: "text/plain", data: DRAG_FORBIDDEN_URL}]], "copy", window);
isnot(gURLBar.value, DRAG_FORBIDDEN_URL, "Shouldn't be allowed to drop forbidden URL on URL bar");
});
@ -27,11 +27,11 @@ add_task(async function checkDragForbiddenURL() {
add_task(async function checkDragText() {
await BrowserTestUtils.withNewTab(TEST_URL, function(browser) {
EventUtils.synthesizeDrop(document.getElementById("downloads-button"), gURLBar,
EventUtils.synthesizeDrop(document.getElementById("home-button"), gURLBar,
[[{type: "text/plain", data: DRAG_TEXT}]], "copy", window);
is(gURLBar.value, DRAG_TEXT, "Dragging normal text should replace the URL bar value");
EventUtils.synthesizeDrop(document.getElementById("downloads-button"), gURLBar,
EventUtils.synthesizeDrop(document.getElementById("home-button"), gURLBar,
[[{type: "text/plain", data: DRAG_WORD}]], "copy", window);
is(gURLBar.value, DRAG_WORD, "Dragging a single word should replace the URL bar value");
});

View File

@ -41,12 +41,12 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
allowevents="true"
inputmode="url"
xbl:inherits="tooltiptext=inputtooltiptext,value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,focused,textoverflow"/>
<xul:image anonid="urlbar-go-button"
class="urlbar-go-button urlbar-icon"
onclick="gURLBar.handleCommand(event);"
tooltiptext="&goEndCap.tooltip;"
xbl:inherits="pageproxystate,parentfocused=focused,usertyping"/>
</xul:hbox>
<xul:image anonid="urlbar-go-button"
class="urlbar-go-button urlbar-icon"
onclick="gURLBar.handleCommand(event);"
tooltiptext="&goEndCap.tooltip;"
xbl:inherits="pageproxystate,parentfocused=focused,usertyping"/>
<xul:dropmarker anonid="historydropmarker"
class="autocomplete-history-dropmarker urlbar-history-dropmarker urlbar-icon"
tooltiptext="&urlbar.openHistoryPopup.tooltip;"
@ -1943,9 +1943,10 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Position the popup below the navbar. To get the y-coordinate,
// which is an offset from the bottom of the input, subtract the
// bottom of the navbar from the buttom of the input.
let yOffset =
let yOffset = Math.round(
this.DOMWindowUtils.getBoundsWithoutFlushing(document.getElementById("nav-bar")).bottom -
this.DOMWindowUtils.getBoundsWithoutFlushing(aInput).bottom;
this.DOMWindowUtils.getBoundsWithoutFlushing(aInput).bottom);
this.openPopup(aElement, "after_start", 0, yOffset, false, false);
]]></body>
</method>

View File

@ -40,6 +40,7 @@ const kPrefCustomizationDebug = "browser.uiCustomization.debug";
const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar";
const kPrefUIDensity = "browser.uidensity";
const kPrefAutoTouchMode = "browser.touchmode.auto";
const kPrefAutoHideDownloadsButton = "browser.download.autohideButton";
const kExpectedWindowURL = "chrome://browser/content/browser.xul";
@ -57,7 +58,7 @@ const kSubviewEvents = [
* The current version. We can use this to auto-add new default widgets as necessary.
* (would be const but isn't because of testing purposes)
*/
var kVersion = 10;
var kVersion = 11;
/**
* Buttons removed from built-ins by version they were removed. kVersion must be
@ -177,7 +178,7 @@ var CustomizableUIInternal = {
this.addListener(this);
this._defineBuiltInWidgets();
this.loadSavedState();
this._introduceNewBuiltinWidgets();
this._updateForNewVersion();
this._markObsoleteBuiltinButtonsSeen();
this.registerArea(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, {
@ -262,7 +263,7 @@ var CustomizableUIInternal = {
}
},
_introduceNewBuiltinWidgets() {
_updateForNewVersion() {
// We should still enter even if gSavedState.currentVersion >= kVersion
// because the per-widget pref facility is independent of versioning.
if (!gSavedState) {
@ -317,7 +318,7 @@ var CustomizableUIInternal = {
CustomizableUI.removeWidgetFromArea("loop-button-throttled");
}
if (currentVersion < 7 && gSavedState && gSavedState.placements &&
if (currentVersion < 7 && gSavedState.placements &&
gSavedState.placements[CustomizableUI.AREA_NAVBAR]) {
let placements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
let newPlacements = ["back-button", "forward-button", "stop-reload-button", "home-button"];
@ -407,7 +408,7 @@ var CustomizableUIInternal = {
}
}
if (currentVersion < 10 && gSavedState && gSavedState.placements) {
if (currentVersion < 10 && gSavedState.placements) {
for (let placements of Object.values(gSavedState.placements)) {
if (placements.includes("webcompat-reporter-button")) {
placements.splice(placements.indexOf("webcompat-reporter-button"), 1);
@ -415,6 +416,37 @@ var CustomizableUIInternal = {
}
}
}
// Move the downloads button to the default position in the navbar if it's
// not there already.
if (currentVersion < 11 && gSavedState.placements) {
let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
// First remove from wherever it currently lives, if anywhere:
for (let placements of Object.values(gSavedState.placements)) {
let existingIndex = placements.indexOf("downloads-button");
if (existingIndex != -1) {
placements.splice(existingIndex, 1);
break; // It can only be in 1 place, so no point looking elsewhere.
}
}
// Now put the button in the navbar in the correct spot:
if (navbarPlacements) {
let insertionPoint = navbarPlacements.indexOf("urlbar-container");
// Deliberately iterate to 1 past the end of the array to insert at the
// end if need be.
while (++insertionPoint < navbarPlacements.length) {
let widget = navbarPlacements[insertionPoint];
// If we find a non-searchbar, non-spacer node, break out of the loop:
if (widget != "search-container" && !this.matchingSpecials(widget, "spring")) {
break;
}
}
// We either found the right spot, or reached the end of the
// placements, so insert here:
navbarPlacements.splice(insertionPoint, 0, "downloads-button");
}
}
},
/**
@ -2597,6 +2629,7 @@ var CustomizableUIInternal = {
gUIStateBeforeReset.uiDensity = Services.prefs.getIntPref(kPrefUIDensity);
gUIStateBeforeReset.autoTouchMode = Services.prefs.getBoolPref(kPrefAutoTouchMode);
gUIStateBeforeReset.currentTheme = LightweightThemeManager.currentTheme;
gUIStateBeforeReset.autoHideDownloadsButton = Services.prefs.getBoolPref(kPrefAutoHideDownloadsButton);
gUIStateBeforeReset.newElementCount = gNewElementCount;
} catch (e) { }
@ -2606,6 +2639,7 @@ var CustomizableUIInternal = {
Services.prefs.clearUserPref(kPrefDrawInTitlebar);
Services.prefs.clearUserPref(kPrefUIDensity);
Services.prefs.clearUserPref(kPrefAutoTouchMode);
Services.prefs.clearUserPref(kPrefAutoHideDownloadsButton);
LightweightThemeManager.currentTheme = null;
gNewElementCount = 0;
log.debug("State reset");
@ -2674,11 +2708,10 @@ var CustomizableUIInternal = {
}
gUndoResetting = true;
let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState;
let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar;
let currentTheme = gUIStateBeforeReset.currentTheme;
let uiDensity = gUIStateBeforeReset.uiDensity;
let autoTouchMode = gUIStateBeforeReset.autoTouchMode;
const {
uiCustomizationState, drawInTitlebar, currentTheme, uiDensity,
autoTouchMode, autoHideDownloadsButton,
} = gUIStateBeforeReset;
gNewElementCount = gUIStateBeforeReset.newElementCount;
// Need to clear the previous state before setting the prefs
@ -2689,6 +2722,7 @@ var CustomizableUIInternal = {
Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar);
Services.prefs.setIntPref(kPrefUIDensity, uiDensity);
Services.prefs.setBoolPref(kPrefAutoTouchMode, autoTouchMode);
Services.prefs.setBoolPref(kPrefAutoHideDownloadsButton, autoHideDownloadsButton);
LightweightThemeManager.currentTheme = currentTheme;
this.loadSavedState();
// If the user just customizes toolbar/titlebar visibility, gSavedState will be null

View File

@ -21,6 +21,10 @@ const kKeepBroadcastAttributes = "keepbroadcastattributeswhencustomizing";
const kPanelItemContextMenu = "customizationPanelItemContextMenu";
const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
const kDownloadAutohideCheckboxId = "downloads-button-autohide-checkbox";
const kDownloadAutohidePanelId = "downloads-button-autohide-panel";
const kDownloadAutoHidePref = "browser.download.autohideButton";
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/CustomizableUI.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -354,6 +358,8 @@ CustomizeMode.prototype = {
this._updateLWThemeButtonIcon();
this._setupDownloadAutoHideToggle();
this._handler.isEnteringCustomizeMode = false;
CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
@ -395,6 +401,8 @@ CustomizeMode.prototype = {
this._removeExtraToolbarsIfEmpty();
this._teardownDownloadAutoHideToggle();
CustomizableUI.removeListener(this);
this.document.removeEventListener("keypress", this);
@ -472,6 +480,8 @@ CustomizeMode.prototype = {
for (let toolbar of customizableToolbars)
toolbar.removeAttribute("customizing");
this._maybeMoveDownloadsButtonToNavBar();
delete this._lastLightweightTheme;
this._changed = false;
this._transitioning = false;
@ -616,6 +626,12 @@ CustomizeMode.prototype = {
if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
aNode = aNode.firstChild;
}
// If the user explicitly moves this item, turn off autohide.
if (aNode.id == "downloads-button") {
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
}
CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_NAVBAR);
if (!this._customizing) {
CustomizableUI.dispatchToolboxEvent("customizationchange");
@ -627,6 +643,12 @@ CustomizeMode.prototype = {
if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
aNode = aNode.firstChild;
}
// If the user explicitly moves this item, turn off autohide.
if (aNode.id == "downloads-button") {
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
}
let panel = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL;
CustomizableUI.addWidgetToArea(aNode.id, panel);
if (!this._customizing) {
@ -655,6 +677,10 @@ CustomizeMode.prototype = {
if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
aNode = aNode.firstChild;
}
// If the user explicitly removes this item, turn off autohide.
if (aNode.id == "downloads-button") {
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
}
CustomizableUI.removeWidgetFromArea(aNode.id);
if (!this._customizing) {
CustomizableUI.dispatchToolboxEvent("customizationchange");
@ -1081,6 +1107,7 @@ CustomizeMode.prototype = {
this._updateResetButton();
this._updateUndoResetButton();
this._updateEmptyPaletteNotice();
this._moveDownloadsButtonToNavBar = false;
this.resetting = false;
if (!this._wantToBeInCustomizeMode) {
this.exit();
@ -1107,6 +1134,7 @@ CustomizeMode.prototype = {
this._updateResetButton();
this._updateUndoResetButton();
this._updateEmptyPaletteNotice();
this._moveDownloadsButtonToNavBar = false;
this.resetting = false;
})().catch(log.error);
},
@ -1173,6 +1201,9 @@ CustomizeMode.prototype = {
this.visiblePalette.appendChild(paletteItem);
}
}
if (aNodeToChange.id == "downloads-button") {
this._showDownloadsAutoHidePanel();
}
},
onWidgetDestroyed(aWidgetId) {
@ -1757,6 +1788,11 @@ CustomizeMode.prototype = {
} catch (ex) {
log.error(ex, ex.stack);
}
// If the user explicitly moves this item, turn off autohide.
if (draggedItemId == "downloads-button") {
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
}
},
_applyDrop(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) {
@ -2246,6 +2282,94 @@ CustomizeMode.prototype = {
doc.getElementById("customizationPanelItemContextMenuUnpin").hidden = !inPermanentArea;
doc.getElementById("customizationPanelItemContextMenuPin").hidden = inPermanentArea;
},
_checkForDownloadsClick(event) {
if (event.target.closest("#wrapper-downloads-button") && event.button == 0) {
event.view.gCustomizeMode._showDownloadsAutoHidePanel();
}
},
_setupDownloadAutoHideToggle() {
this.document.getElementById(kDownloadAutohidePanelId).removeAttribute("hidden");
this.window.addEventListener("click", this._checkForDownloadsClick, true);
},
_teardownDownloadAutoHideToggle() {
this.window.removeEventListener("click", this._checkForDownloadsClick, true);
this.document.getElementById(kDownloadAutohidePanelId).hidePopup();
},
_maybeMoveDownloadsButtonToNavBar() {
// If the user toggled the autohide checkbox while the item was in the
// palette, and hasn't moved it since, move the item to the default
// location in the navbar for them.
if (!CustomizableUI.getPlacementOfWidget("downloads-button") &&
this._moveDownloadsButtonToNavBar &&
this.window.DownloadsButton.autoHideDownloadsButton) {
let navbarPlacements = CustomizableUI.getWidgetIdsInArea("nav-bar");
let insertionPoint = navbarPlacements.indexOf("urlbar-container");
while (++insertionPoint < navbarPlacements.length) {
let widget = navbarPlacements[insertionPoint];
// If we find a non-searchbar, non-spacer node, break out of the loop:
if (widget != "search-container" &&
!(CustomizableUI.isSpecialWidget(widget) && widget.includes("spring"))) {
break;
}
}
CustomizableUI.addWidgetToArea("downloads-button", "nav-bar", insertionPoint);
}
},
_showDownloadsAutoHidePanel() {
let doc = this.document;
let panel = doc.getElementById(kDownloadAutohidePanelId);
panel.hidePopup();
let button = doc.getElementById("downloads-button");
// We don't show the tooltip if the button is in the panel.
if (button.closest("#widget-overflow-fixed-list")) {
return;
}
let checkbox = doc.getElementById(kDownloadAutohideCheckboxId);
if (this.window.DownloadsButton.autoHideDownloadsButton) {
checkbox.setAttribute("checked", "true");
} else {
checkbox.removeAttribute("checked");
}
let offsetX = 0, offsetY = 0;
let position;
if (button.closest("#nav-bar")) {
let navbarWidgets = CustomizableUI.getWidgetIdsInArea("nav-bar");
if (navbarWidgets.indexOf("urlbar-container") > navbarWidgets.indexOf("downloads-button")) {
// Tested in RTL, these get inverted automatically, so this does the
// right thing without taking RTL into account explicitly.
position = "rightcenter topleft";
offsetX = -8;
} else {
position = "leftcenter topright";
offsetX = 8;
}
} else if (button.closest("#customization-palette")) {
position = "topcenter bottomleft";
offsetY = 10;
} else {
// For non-navbar toolbars, this works better than guessing whether
// left or right is a better place to position:
position = "bottomcenter topleft";
offsetY = -5;
}
// We don't use the icon to anchor because it might be resizing because of
// the animations for drag/drop. Hence the use of offsets.
panel.openPopup(button, position, offsetX, offsetY);
},
onDownloadsAutoHideChange(event) {
let checkbox = event.target.ownerDocument.getElementById(kDownloadAutohideCheckboxId);
Services.prefs.setBoolPref(kDownloadAutoHidePref, checkbox.checked);
// Ensure we move the button (back) after the user leaves customize mode.
event.view.gCustomizeMode._moveDownloadsButtonToNavBar = checkbox.checked;
},
};
function __dumpDragData(aEvent, caller) {

View File

@ -16,6 +16,13 @@ XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
const TRANSITION_PHASES = Object.freeze({
START: 1,
PREPARE: 2,
TRANSITION: 3,
END: 4
});
/**
* Simple implementation of the sliding window pattern; panels are added to a
* linked list, in-order, and the currently shown panel is remembered using a
@ -341,7 +348,8 @@ this.PanelMultiView = class {
this._panel.removeEventListener("popuphidden", this);
this.window.removeEventListener("keydown", this);
this.node = this._clickCapturer = this._viewContainer = this._mainViewContainer =
this._subViews = this._viewStack = this.__dwu = this._panelViewCache = null;
this._subViews = this._viewStack = this.__dwu = this._panelViewCache =
this._transitionDetails = null;
}
/**
@ -423,6 +431,8 @@ this.PanelMultiView = class {
this.node.setAttribute("viewtype", "main");
});
}
} else if (this.panelViews) {
this._mainView.setAttribute("current", "true");
}
if (!this.panelViews) {
@ -431,12 +441,11 @@ this.PanelMultiView = class {
}
showSubView(aViewId, aAnchor, aPreviousView) {
const {document, window} = this;
return (async () => {
// Support passing in the node directly.
let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
if (!viewNode) {
viewNode = document.getElementById(aViewId);
viewNode = this.document.getElementById(aViewId);
if (viewNode) {
this._placeSubView(viewNode);
} else {
@ -448,7 +457,8 @@ this.PanelMultiView = class {
let reverse = !!aPreviousView;
let previousViewNode = aPreviousView || this._currentSubView;
let playTransition = (!!previousViewNode && previousViewNode != viewNode);
let playTransition = (!!previousViewNode && previousViewNode != viewNode &&
this._panel.state == "open");
let dwu, previousRect;
if (playTransition || this.panelViews) {
@ -485,7 +495,8 @@ this.PanelMultiView = class {
viewNode.style.maxWidth = viewNode.style.minWidth = this._mainViewWidth + "px";
// Emit the ViewShowing event so that the widget definition has a chance
// to lazily populate the subview with things.
// to lazily populate the subview with things or perhaps even cancel this
// whole operation.
let detail = {
blockers: new Set(),
addBlocker(promise) {
@ -509,153 +520,30 @@ this.PanelMultiView = class {
}
this._currentSubView = viewNode;
viewNode.setAttribute("current", true);
if (this.panelViews) {
if (viewNode.id == this._mainViewId) {
this.node.setAttribute("viewtype", "main");
} else {
this.node.setAttribute("viewtype", "subview");
}
if (!playTransition)
if (!playTransition) {
viewNode.setAttribute("current", true);
this.descriptionHeightWorkaround(viewNode);
}
}
// Now we have to transition the panel. There are a few parts to this:
//
// 1) The main view content gets shifted so that the center of the anchor
// node is at the left-most edge of the panel.
// 2) The subview deck slides in so that it takes up almost all of the
// panel.
// 3) If the subview is taller then the main panel contents, then the panel
// must grow to meet that new height. Otherwise, it must shrink.
//
// All three of these actions make use of CSS transformations, so they
// should all occur simultaneously.
// Now we have to transition the panel.
if (this.panelViews && playTransition) {
// Sliding the next subview in means that the previous panelview stays
// where it is and the active panelview slides in from the left in LTR
// mode, right in RTL mode.
let onTransitionEnd = () => {
this._dispatchViewEvent(previousViewNode, "ViewHiding");
previousViewNode.removeAttribute("current");
this.descriptionHeightWorkaround(viewNode);
};
// There's absolutely no need to show off our epic animation skillz when
// the panel's not even open.
if (this._panel.state != "open") {
onTransitionEnd();
return;
}
if (aAnchor)
aAnchor.setAttribute("open", true);
// Set the viewContainer dimensions to make sure only the current view
// is visible.
this._viewContainer.style.height = Math.max(previousRect.height, this._mainViewHeight) + "px";
this._viewContainer.style.width = previousRect.width + "px";
// Lock the dimensions of the window that hosts the popup panel.
let rect = this._panel.popupBoxObject.getOuterScreenRect();
this._panel.setAttribute("width", rect.width);
this._panel.setAttribute("height", rect.height);
await this._transitionViews(previousViewNode, viewNode, reverse, previousRect);
this._viewBoundsOffscreen(viewNode, previousRect, viewRect => {
this._transitioning = true;
if (this._autoResizeWorkaroundTimer)
window.clearTimeout(this._autoResizeWorkaroundTimer);
this._viewContainer.setAttribute("transition-reverse", reverse);
let nodeToAnimate = reverse ? previousViewNode : viewNode;
if (aAnchor)
aAnchor.removeAttribute("open");
if (!reverse) {
// We set the margin here to make sure the view is positioned next
// to the view that is currently visible. The animation is taken
// care of by transitioning the `transform: translateX()` property
// instead.
// Once the transition finished, we clean both properties up.
nodeToAnimate.style.marginInlineStart = previousRect.width + "px";
}
// Set the transition style and listen for its end to clean up and
// make sure the box sizing becomes dynamic again.
// Somehow, putting these properties in PanelUI.css doesn't work for
// newly shown nodes in a XUL parent node.
nodeToAnimate.style.transition = "transform ease-" + (reverse ? "in" : "out") +
" var(--panelui-subview-transition-duration)";
nodeToAnimate.style.willChange = "transform";
nodeToAnimate.style.borderInlineStart = "1px solid var(--panel-separator-color)";
// Wait until after the first paint to ensure setting 'current=true'
// has taken full effect; once both views are visible, we want to
// correctly measure rects using `dwu.getBoundsWithoutFlushing`.
window.addEventListener("MozAfterPaint", () => {
if (this._panel.state != "open") {
onTransitionEnd();
return;
}
// Now set the viewContainer dimensions to that of the new view, which
// kicks of the height animation.
this._viewContainer.style.height = Math.max(viewRect.height, this._mainViewHeight) + "px";
this._viewContainer.style.width = viewRect.width + "px";
this._panel.removeAttribute("width");
this._panel.removeAttribute("height");
// The 'magic' part: build up the amount of pixels to move right or left.
let moveToLeft = (this._dir == "rtl" && !reverse) || (this._dir == "ltr" && reverse);
let movementX = reverse ? viewRect.width : previousRect.width;
let moveX = (moveToLeft ? "" : "-") + movementX;
nodeToAnimate.style.transform = "translateX(" + moveX + "px)";
// We're setting the width property to prevent flickering during the
// sliding animation with smaller views.
nodeToAnimate.style.width = viewRect.width + "px";
this._viewContainer.addEventListener("transitionend", this._transitionEndListener = ev => {
// It's quite common that `height` on the view container doesn't need
// to transition, so we make sure to do all the work on the transform
// transition-end, because that is guaranteed to happen.
if (ev.target != nodeToAnimate || ev.propertyName != "transform")
return;
this._viewContainer.removeEventListener("transitionend", this._transitionEndListener);
this._transitionEndListener = null;
onTransitionEnd();
this._transitioning = false;
if (reverse) {
this._resetKeyNavigation(previousViewNode);
}
// Myeah, panel layout auto-resizing is a funky thing. We'll wait
// another few milliseconds to remove the width and height 'fixtures',
// to be sure we don't flicker annoyingly.
// NB: HACK! Bug 1363756 is there to fix this.
this._autoResizeWorkaroundTimer = window.setTimeout(() => {
this._viewContainer.style.removeProperty("height");
this._viewContainer.style.removeProperty("width");
}, 500);
// Take another breather, just like before, to wait for the 'current'
// attribute removal to take effect. This prevents a flicker.
// The cleanup we do doesn't affect the display anymore, so we're not
// too fussed about the timing here.
window.addEventListener("MozAfterPaint", () => {
nodeToAnimate.style.removeProperty("border-inline-start");
nodeToAnimate.style.removeProperty("transition");
nodeToAnimate.style.removeProperty("transform");
nodeToAnimate.style.removeProperty("width");
if (!reverse)
viewNode.style.removeProperty("margin-inline-start");
if (aAnchor)
aAnchor.removeAttribute("open");
this._viewContainer.removeAttribute("transition-reverse");
this._dispatchViewEvent(viewNode, "ViewShown");
this._updateKeyboardFocus(viewNode);
}, { once: true });
});
}, { once: true });
});
this._dispatchViewEvent(viewNode, "ViewShown");
this._updateKeyboardFocus(viewNode);
} else if (!this.panelViews) {
this._transitionHeight(() => {
viewNode.setAttribute("current", true);
@ -674,6 +562,198 @@ this.PanelMultiView = class {
})().catch(e => Cu.reportError(e));
}
/**
* Apply a transition to 'slide' from the currently active view to the next
* one.
* Sliding the next subview in means that the previous panelview stays where it
* is and the active panelview slides in from the left in LTR mode, right in
* RTL mode.
*
* @param {panelview} previousViewNode Node that is currently shown as active,
* but is about to be transitioned away.
* @param {panelview} viewNode Node that will becode the active view,
* after the transition has finished.
* @param {Boolean} reverse Whether we're navigation back to a
* previous view or forward to a next view.
* @param {Object} previousRect Rect object, with the same structure as
* a DOMRect, of the `previousViewNode`.
* @param {Function} callback Function that will be invoked when the
* transition is finished or when the
* operation was canceled (early return).
*/
async _transitionViews(previousViewNode, viewNode, reverse, previousRect) {
// There's absolutely no need to show off our epic animation skillz when
// the panel's not even open.
if (this._panel.state != "open") {
return;
}
const {window, document} = this;
if (this._autoResizeWorkaroundTimer)
window.clearTimeout(this._autoResizeWorkaroundTimer);
this._transitionDetails = {
phase: TRANSITION_PHASES.START,
previousViewNode, viewNode, reverse
};
// Set the viewContainer dimensions to make sure only the current view is
// visible.
this._viewContainer.style.height = Math.max(previousRect.height, this._mainViewHeight) + "px";
this._viewContainer.style.width = previousRect.width + "px";
// Lock the dimensions of the window that hosts the popup panel.
let rect = this._panel.popupBoxObject.getOuterScreenRect();
this._panel.setAttribute("width", rect.width);
this._panel.setAttribute("height", rect.height);
let viewRect;
if (viewNode.__lastKnownBoundingRect) {
viewRect = viewNode.__lastKnownBoundingRect;
viewNode.setAttribute("current", true);
} else if (viewNode.customRectGetter) {
// Can't use Object.assign directly with a DOM Rect object because its properties
// aren't enumerable.
let {height, width} = previousRect;
viewRect = Object.assign({height, width}, viewNode.customRectGetter());
let {header} = viewNode;
if (header) {
viewRect.height += this._dwu.getBoundsWithoutFlushing(header).height;
}
viewNode.setAttribute("current", true);
} else {
let oldSibling = viewNode.nextSibling || null;
this._offscreenViewStack.appendChild(viewNode);
viewNode.setAttribute("current", true);
viewRect = await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {
return this._dwu.getBoundsWithoutFlushing(viewNode);
});
try {
this._viewStack.insertBefore(viewNode, oldSibling);
} catch (ex) {
this._viewStack.appendChild(viewNode);
}
}
this._transitioning = true;
this._transitionDetails.phase = TRANSITION_PHASES.PREPARE;
// The 'magic' part: build up the amount of pixels to move right or left.
let moveToLeft = (this._dir == "rtl" && !reverse) || (this._dir == "ltr" && reverse);
let deltaX = previousRect.width;
let deepestNode = reverse ? previousViewNode : viewNode;
// With a transition when navigating backwards - user hits the 'back'
// button - we need to make sure that the views are positioned in a way
// that a translateX() unveils the previous view from the right direction.
if (reverse)
this._viewStack.style.marginInlineStart = "-" + deltaX + "px";
// Set the transition style and listen for its end to clean up and make sure
// the box sizing becomes dynamic again.
// Somehow, putting these properties in PanelUI.css doesn't work for newly
// shown nodes in a XUL parent node.
this._viewStack.style.transition = "transform var(--animation-easing-function)" +
" var(--panelui-subview-transition-duration)";
this._viewStack.style.willChange = "transform";
deepestNode.style.borderInlineStart = "1px solid var(--panel-separator-color)";
// Now set the viewContainer dimensions to that of the new view, which
// kicks of the height animation.
this._viewContainer.style.height = Math.max(viewRect.height, this._mainViewHeight) + "px";
this._viewContainer.style.width = viewRect.width + "px";
this._panel.removeAttribute("width");
this._panel.removeAttribute("height");
// We're setting the width property to prevent flickering during the
// sliding animation with smaller views.
viewNode.style.width = viewRect.width + "px";
await BrowserUtils.promiseLayoutFlushed(document, "layout", () => {});
// Kick off the transition!
this._transitionDetails.phase = TRANSITION_PHASES.TRANSITION;
this._viewStack.style.transform = "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
await new Promise(resolve => {
this._transitionDetails.resolve = resolve;
this._viewContainer.addEventListener("transitionend", this._transitionDetails.listener = ev => {
// It's quite common that `height` on the view container doesn't need
// to transition, so we make sure to do all the work on the transform
// transition-end, because that is guaranteed to happen.
if (ev.target != this._viewStack || ev.propertyName != "transform")
return;
this._viewContainer.removeEventListener("transitionend", this._transitionDetails.listener);
delete this._transitionDetails.listener;
resolve();
});
});
this._transitionDetails.phase = TRANSITION_PHASES.END;
await this._cleanupTransitionPhase();
}
/**
* Attempt to clean up the attributes and properties set by `_transitionViews`
* above. Which attributes and properties depends on the phase the transition
* was left from - normally that'd be `TRANSITION_PHASES.END`.
*/
async _cleanupTransitionPhase() {
if (!this._transitionDetails)
return;
let {phase, previousViewNode, viewNode, reverse, resolve, listener} = this._transitionDetails;
this._transitionDetails = null;
// Do the things we _always_ need to do whenever the transition ends or is
// interrupted.
this._dispatchViewEvent(previousViewNode, "ViewHiding");
previousViewNode.removeAttribute("current");
if (reverse)
this._resetKeyNavigation(previousViewNode);
this.descriptionHeightWorkaround(viewNode);
if (phase >= TRANSITION_PHASES.START) {
this._panel.removeAttribute("width");
this._panel.removeAttribute("height");
// Myeah, panel layout auto-resizing is a funky thing. We'll wait
// another few milliseconds to remove the width and height 'fixtures',
// to be sure we don't flicker annoyingly.
// NB: HACK! Bug 1363756 is there to fix this.
this._autoResizeWorkaroundTimer = this.window.setTimeout(() => {
if (!this._viewContainer)
return;
this._viewContainer.style.removeProperty("height");
this._viewContainer.style.removeProperty("width");
}, 500);
}
if (phase >= TRANSITION_PHASES.PREPARE) {
this._transitioning = false;
if (reverse)
this._viewStack.style.removeProperty("margin-inline-start");
let deepestNode = reverse ? previousViewNode : viewNode;
deepestNode.style.removeProperty("border-inline-start");
this._viewStack.style.removeProperty("transition");
}
if (phase >= TRANSITION_PHASES.TRANSITION) {
this._viewStack.style.removeProperty("transform");
viewNode.style.removeProperty("width");
if (listener)
this._viewContainer.removeEventListener("transitionend", listener);
if (resolve)
resolve();
}
if (phase >= TRANSITION_PHASES.END) {
// We force 'display: none' on the previous view node to make sure that it
// doesn't cause an annoying flicker whilst resetting the styles above.
previousViewNode.style.display = "none";
await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {});
previousViewNode.style.removeProperty("display");
}
}
/**
* Helper method to emit an event on a panelview, whilst also making sure that
* the correct method is called on CustomizableWidget instances.
@ -703,51 +783,6 @@ this.PanelMultiView = class {
return cancel;
}
/**
* Calculate the correct bounds of a panelview node offscreen to minimize the
* amount of paint flashing and keep the stack vs panel layouts from interfering.
*
* @param {panelview} viewNode Node to measure the bounds of.
* @param {Rect} previousRect Rect representing the previous view
* (used to fill in any blanks).
* @param {Function} callback Called when we got the measurements in and pass
* them on as its first argument.
*/
_viewBoundsOffscreen(viewNode, previousRect, callback) {
if (viewNode.__lastKnownBoundingRect) {
callback(viewNode.__lastKnownBoundingRect);
return;
}
if (viewNode.customRectGetter) {
// Can't use Object.assign directly with a DOM Rect object because its properties
// aren't enumerable.
let {height, width} = previousRect;
let rect = Object.assign({height, width}, viewNode.customRectGetter());
let {header} = viewNode;
if (header) {
rect.height += this._dwu.getBoundsWithoutFlushing(header).height;
}
callback(rect);
return;
}
let oldSibling = viewNode.nextSibling || null;
this._offscreenViewStack.appendChild(viewNode);
BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {
return this._dwu.getBoundsWithoutFlushing(viewNode);
}).then(viewRect => {
try {
this._viewStack.insertBefore(viewNode, oldSibling);
} catch (ex) {
this._viewStack.appendChild(viewNode);
}
callback(viewRect);
});
}
/**
* Applies the height transition for which <panelmultiview> is designed.
*
@ -962,10 +997,7 @@ this.PanelMultiView = class {
this.node.removeAttribute("panelopen");
this.showMainView();
if (this.panelViews) {
if (this._transitionEndListener) {
this._viewContainer.removeEventListener("transitionend", this._transitionEndListener);
this._transitionEndListener = null;
}
this._cleanupTransitionPhase();
for (let panelView of this._viewStack.children) {
if (panelView.nodeName != "children") {
panelView.__lastKnownBoundingRect = null;

View File

@ -29,15 +29,11 @@
.panel-viewcontainer[panelopen]:-moz-any(:not([viewtype="main"]),[transitioning]) {
transition-property: height;
transition-timing-function: ease-in;
transition-timing-function: var(--animation-easing-function);
transition-duration: var(--panelui-subview-transition-duration);
will-change: height;
}
.panel-viewcontainer[panelopen]:-moz-any(:not([viewtype="main"]),[transitioning])[transition-reverse] {
transition-timing-function: ease-out;
}
/* START photon adjustments */
photonpanelmultiview > .panel-viewcontainer > .panel-viewstack {

View File

@ -753,3 +753,17 @@
</panelview>
</photonpanelmultiview>
</panel>
<panel id="downloads-button-autohide-panel"
role="group"
type="arrow"
hidden="true"
onpopupshown="gCustomizeMode._downloadPanelAutoHideTimeout = setTimeout(() => event.target.hidePopup(), 4000);"
onmouseover="clearTimeout(gCustomizeMode._downloadPanelAutoHideTimeout);"
onmouseout="gCustomizeMode._downloadPanelAutoHideTimeout = setTimeout(() => event.target.hidePopup(), 2000);"
onpopuphidden="clearTimeout(gCustomizeMode._downloadPanelAutoHideTimeout);"
>
<checkbox id="downloads-button-autohide-checkbox"
label="&customizeMode.autoHideDownloadsButton.label;" checked="true"
oncommand="gCustomizeMode.onDownloadsAutoHideChange(event)"/>
</panel>

View File

@ -54,9 +54,9 @@
<binding id="photonpanelmultiview" extends="chrome://browser/content/customizableui/panelUI.xml#panelmultiview">
<content>
<xul:box anonid="viewContainer" class="panel-viewcontainer" xbl:inherits="panelopen,transitioning">
<xul:stack anonid="viewStack" xbl:inherits="transitioning" class="panel-viewstack">
<xul:box anonid="viewStack" xbl:inherits="transitioning" class="panel-viewstack">
<children includes="panelview"/>
</xul:stack>
</xul:box>
</xul:box>
<xul:box class="panel-viewcontainer offscreen">
<xul:box anonid="offscreenViewStack" class="panel-viewstack"/>

View File

@ -19,7 +19,7 @@ function test() {
"All future placements should be dealt with by now.");
let {CustomizableUIInternal, gFuturePlacements, gPalette} = CustomizableUIBSPass;
CustomizableUIInternal._introduceNewBuiltinWidgets();
CustomizableUIInternal._updateForNewVersion();
is(gFuturePlacements.size, 0,
"No change to future placements initially.");
@ -67,7 +67,7 @@ function test() {
}
// Then call the re-init routine so we re-add the builtin widgets
CustomizableUIInternal._introduceNewBuiltinWidgets();
CustomizableUIInternal._updateForNewVersion();
is(gFuturePlacements.size, 1,
"Should have 1 more future placement");
let haveNavbarPlacements = gFuturePlacements.has(CustomizableUI.AREA_NAVBAR);
@ -96,9 +96,11 @@ function test() {
is(placements[0], testWidgetNew.id, "Should have our test widget to be placed in nav-bar");
}
// Now test that the builtin photon migrations work:
// Reset kVersion
CustomizableUIBSPass.kVersion--;
// Now test that the builtin photon migrations work:
CustomizableUIBSPass.gSavedState = {
currentVersion: 6,
placements: {
@ -106,7 +108,7 @@ function test() {
"PanelUI-contents": ["panic-button", "edit-controls"],
},
};
CustomizableUIInternal._introduceNewBuiltinWidgets();
CustomizableUIInternal._updateForNewVersion();
let navbarPlacements = CustomizableUIBSPass.gSavedState.placements["nav-bar"];
let springs = navbarPlacements.filter(id => id.includes("spring"));
is(springs.length, 2, "Should have 2 toolbarsprings in placements now");
@ -116,13 +118,61 @@ function test() {
is(navbarPlacements[2], "stop-reload-button", "Stop/reload button is in the right place.");
is(navbarPlacements[3], "home-button", "Home button is in the right place.");
is(navbarPlacements[4], "urlbar-container", "URL bar is in the right place.");
is(navbarPlacements[5], "library-button", "Library button is in the right place.");
is(navbarPlacements[6], "sidebar-button", "Sidebar button is in the right place.");
is(navbarPlacements.length, 7, "Should have 7 items");
is(navbarPlacements[5], "downloads-button", "Downloads button is in the right place.");
is(navbarPlacements[6], "library-button", "Library button is in the right place.");
is(navbarPlacements[7], "sidebar-button", "Sidebar button is in the right place.");
is(navbarPlacements.length, 8, "Should have 8 items");
let overflowPlacements = CustomizableUIBSPass.gSavedState.placements["widget-overflow-fixed-list"];
Assert.deepEqual(overflowPlacements, ["panic-button"]);
// Finally test that the downloads migration works:
let oldNavbarPlacements = [
"urlbar-container", "customizableui-special-spring3", "search-container",
];
CustomizableUIBSPass.gSavedState = {
currentVersion: 10,
placements: {
"nav-bar": Array.from(oldNavbarPlacements),
"widget-overflow-fixed-list": ["downloads-button"],
},
};
CustomizableUIInternal._updateForNewVersion();
navbarPlacements = CustomizableUIBSPass.gSavedState.placements["nav-bar"];
Assert.deepEqual(navbarPlacements, oldNavbarPlacements.concat(["downloads-button"]),
"Downloads button inserted in navbar");
Assert.deepEqual(CustomizableUIBSPass.gSavedState.placements["widget-overflow-fixed-list"], [],
"Overflow panel is empty");
CustomizableUIBSPass.gSavedState = {
currentVersion: 10,
placements: {
"nav-bar": ["downloads-button"].concat(oldNavbarPlacements),
},
};
CustomizableUIInternal._updateForNewVersion();
navbarPlacements = CustomizableUIBSPass.gSavedState.placements["nav-bar"];
Assert.deepEqual(navbarPlacements, oldNavbarPlacements.concat(["downloads-button"]),
"Downloads button reinserted in navbar");
oldNavbarPlacements = [
"urlbar-container", "customizableui-special-spring3", "search-container", "other-widget",
];
CustomizableUIBSPass.gSavedState = {
currentVersion: 10,
placements: {
"nav-bar": Array.from(oldNavbarPlacements),
},
};
CustomizableUIInternal._updateForNewVersion();
navbarPlacements = CustomizableUIBSPass.gSavedState.placements["nav-bar"];
let expectedNavbarPlacements = [
"urlbar-container", "customizableui-special-spring3", "search-container",
"downloads-button", "other-widget",
];
Assert.deepEqual(navbarPlacements, expectedNavbarPlacements,
"Downloads button inserted in navbar before other widgets");
gFuturePlacements.delete(CustomizableUI.AREA_NAVBAR);
gPalette.delete(testWidgetNew.id);
gPalette.delete(testWidgetOld.id);

View File

@ -17,7 +17,7 @@ function test() {
CustomizableUIInternal.saveState();
CustomizableUIInternal.loadSavedState();
CustomizableUIInternal._introduceNewBuiltinWidgets();
CustomizableUIInternal._updateForNewVersion();
is(gFuturePlacements.size, 0,
"No change to future placements initially.");
@ -44,7 +44,7 @@ function test() {
let savedPlacements = CustomizableUIBSPass.gSavedState.placements[CustomizableUI.AREA_NAVBAR];
// Then call the re-init routine so we re-add the builtin widgets
CustomizableUIInternal._introduceNewBuiltinWidgets();
CustomizableUIInternal._updateForNewVersion();
is(gFuturePlacements.size, 1,
"Should have 1 more future placement");
let futureNavbarPlacements = gFuturePlacements.get(CustomizableUI.AREA_NAVBAR);

View File

@ -16,17 +16,17 @@ add_task(async function() {
skippedItem.setAttribute("skipintoolbarset", "true");
skippedItem.setAttribute("removable", "true");
navbar.customizationTarget.appendChild(skippedItem);
let downloadsButton = document.getElementById("downloads-button");
let libraryButton = document.getElementById("library-button");
await startCustomizing();
ok(CustomizableUI.inDefaultState, "Should still be in default state");
simulateItemDrag(skippedItem, downloadsButton);
simulateItemDrag(skippedItem, libraryButton);
ok(CustomizableUI.inDefaultState, "Should still be in default state");
let skippedItemWrapper = skippedItem.parentNode;
is(skippedItemWrapper.nextSibling && skippedItemWrapper.nextSibling.id,
downloadsButton.parentNode.id, "Should be next to downloads button");
simulateItemDrag(downloadsButton, skippedItem);
let downloadWrapper = downloadsButton.parentNode;
is(downloadWrapper.nextSibling && downloadWrapper.nextSibling.id,
libraryButton.parentNode.id, "Should be next to library button");
simulateItemDrag(libraryButton, skippedItem);
let libraryWrapper = libraryButton.parentNode;
is(libraryWrapper.nextSibling && libraryWrapper.nextSibling.id,
skippedItem.parentNode.id, "Should be next to skipintoolbarset item");
ok(CustomizableUI.inDefaultState, "Should still be in default state");
});

View File

@ -8,11 +8,11 @@
add_task(async function() {
await startCustomizing();
let devButton = document.getElementById("developer-button");
let downloadsButton = document.getElementById("downloads-button");
let libraryButton = document.getElementById("library-button");
let homeButton = document.getElementById("home-button");
let palette = document.getElementById("customization-palette");
ok(devButton && downloadsButton && homeButton && palette, "Stuff should exist");
simulateItemDrag(devButton, downloadsButton);
ok(devButton && libraryButton && homeButton && palette, "Stuff should exist");
simulateItemDrag(devButton, libraryButton);
simulateItemDrag(homeButton, palette);
await gCustomizeMode.reset();
ok(CustomizableUI.inDefaultState, "Should be back in default state");

View File

@ -10,11 +10,11 @@ const kTestToolbarId = "test-empty-drag";
add_task(async function() {
await createToolbarWithPlacements(kTestToolbarId, []);
await startCustomizing();
let downloadButton = document.getElementById("downloads-button");
let libraryButton = document.getElementById("library-button");
let customToolbar = document.getElementById(kTestToolbarId);
simulateItemDrag(downloadButton, customToolbar);
assertAreaPlacements(kTestToolbarId, ["downloads-button"]);
ok(downloadButton.parentNode && downloadButton.parentNode.parentNode == customToolbar,
simulateItemDrag(libraryButton, customToolbar);
assertAreaPlacements(kTestToolbarId, ["library-button"]);
ok(libraryButton.parentNode && libraryButton.parentNode.parentNode == customToolbar,
"Button should really be in toolbar");
await endCustomizing();
removeCustomToolbars();

View File

@ -44,12 +44,12 @@ add_task(async function() {
// Drag an item and drop it onto the nav-bar customization target, but
// not over a particular item.
await startCustomizing();
let downloadsButton = document.getElementById("downloads-button");
simulateItemDrag(downloadsButton, navbar.customizationTarget);
let homeButton = document.getElementById("home-button");
simulateItemDrag(homeButton, navbar.customizationTarget);
await endCustomizing();
is(downloadsButton.previousSibling.id, lastVisible.id,
is(homeButton.previousSibling.id, lastVisible.id,
"The downloads button should be placed after the last visible item.");
await resetCustomization();

View File

@ -3,12 +3,14 @@
const kOverflowPanel = document.getElementById("widget-overflow");
var gOriginalWidth;
registerCleanupFunction(async function() {
async function stopOverflowing() {
kOverflowPanel.removeAttribute("animate");
window.resizeTo(gOriginalWidth, window.outerHeight);
await waitForCondition(() => !document.getElementById("nav-bar").hasAttribute("overflowing"));
CustomizableUI.reset();
});
}
registerCleanupFunction(stopOverflowing);
/**
* This checks that subview-compatible items show up as subviews rather than
@ -42,6 +44,7 @@ add_task(async function check_developer_subview_in_overflow() {
is(developerView.closest("panel"), expectedPanel, "Should be inside the panel");
expectedPanel.hidePopup();
await Promise.resolve(); // wait for popup to hide fully.
await stopOverflowing();
});
/**
@ -51,14 +54,15 @@ add_task(async function check_developer_subview_in_overflow() {
* simplify some of the subview anchoring code.
*/
add_task(async function check_downloads_panel_in_overflow() {
let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
ok(navbar.hasAttribute("overflowing"), "Should still be overflowing");
let button = document.getElementById("downloads-button");
gCustomizeMode.addToPanel(button);
await waitForOverflowButtonShown();
let chevron = document.getElementById("nav-bar-overflow-button");
let shownPanelPromise = promisePanelElementShown(window, kOverflowPanel);
chevron.click();
await shownPanelPromise;
let button = document.getElementById("downloads-button");
button.click();
await waitForCondition(() => {
let panel = document.getElementById("downloadsPanel");

View File

@ -223,6 +223,9 @@ var DownloadsPanel = {
return;
}
// As a belt-and-suspenders check, ensure the button is not hidden.
DownloadsButton.unhide();
this.initialize(() => {
// Delay displaying the panel because this function will sometimes be
// called while another window is closing (like the window for selecting

View File

@ -54,6 +54,11 @@ const DownloadsButton = {
return document.getElementById("downloads-button");
},
/**
* Indicates whether toolbar customization is in progress.
*/
_customizing: false,
/**
* This function is called asynchronously just after window initialization.
*
@ -64,34 +69,6 @@ const DownloadsButton = {
DownloadsIndicatorView.ensureInitialized();
},
/**
* Indicates whether toolbar customization is in progress.
*/
_customizing: false,
/**
* This function is called when toolbar customization starts.
*
* During customization, we never show the actual download progress indication
* or the event notifications, but we show a neutral placeholder. The neutral
* placeholder is an ordinary button defined in the browser window that can be
* moved freely between the toolbars and the customization palette.
*/
customizeStart() {
// Prevent the indicator from being displayed as a temporary anchor
// during customization, even if requested using the getAnchor method.
this._customizing = true;
this._anchorRequested = false;
},
/**
* This function is called when toolbar customization ends.
*/
customizeDone() {
this._customizing = false;
DownloadsIndicatorView.afterCustomize();
},
/**
* Determines the position where the indicator should appear, and moves its
* associated element to the new position.
@ -108,35 +85,16 @@ const DownloadsButton = {
indicator.open = this._anchorRequested;
let widget = CustomizableUI.getWidget("downloads-button")
.forWindow(window);
let widget = CustomizableUI.getWidget("downloads-button");
// Determine if the indicator is located on an invisible toolbar.
if (!isElementVisible(indicator.parentNode) && !widget.overflowed) {
if (!isElementVisible(indicator.parentNode) &&
widget.areaType == CustomizableUI.TYPE_TOOLBAR) {
return null;
}
return DownloadsIndicatorView.indicatorAnchor;
},
/**
* Checks whether the indicator is, or will soon be visible in the browser
* window.
*
* @param aCallback
* Called once the indicator overlay has loaded. Gets a boolean
* argument representing the indicator visibility.
*/
checkIsVisible(aCallback) {
DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, () => {
if (!this._placeholder) {
aCallback(false);
} else {
let element = DownloadsIndicatorView.indicator || this._placeholder;
aCallback(isElementVisible(element.parentNode));
}
});
},
/**
* Indicates whether we should try and show the indicator temporarily as an
* anchor for the panel, even if the indicator would be hidden by default.
@ -172,6 +130,91 @@ const DownloadsButton = {
this._getAnchorInternal();
},
unhide() {
let button = this._placeholder;
if (button && button.hasAttribute("hidden")) {
button.removeAttribute("hidden");
if (this._navBar.contains(button)) {
this._navBar.setAttribute("downloadsbuttonshown", "true");
}
}
},
hide() {
let button = this._placeholder;
if (this.autoHideDownloadsButton && button && button.closest("toolbar")) {
DownloadsPanel.hidePanel();
button.setAttribute("hidden", "true");
this._navBar.removeAttribute("downloadsbuttonshown");
}
},
startAutoHide() {
if (DownloadsIndicatorView.hasDownloads) {
this.unhide();
} else {
this.hide();
}
},
checkForAutoHide() {
let button = this._placeholder;
if (!this._customizing && this.autoHideDownloadsButton &&
button && button.closest("toolbar")) {
this.startAutoHide();
} else {
this.unhide();
}
},
// Callback from CustomizableUI when nodes get moved around.
// We use this to track whether our node has moved somewhere
// where we should (not) autohide it.
onWidgetAfterDOMChange(node) {
if (node == this._placeholder) {
this.checkForAutoHide();
}
},
/**
* This function is called when toolbar customization starts.
*
* During customization, we never show the actual download progress indication
* or the event notifications, but we show a neutral placeholder. The neutral
* placeholder is an ordinary button defined in the browser window that can be
* moved freely between the toolbars and the customization palette.
*/
onCustomizeStart(win) {
if (win == window) {
// Prevent the indicator from being displayed as a temporary anchor
// during customization, even if requested using the getAnchor method.
this._customizing = true;
this._anchorRequested = false;
this.unhide();
}
},
onCustomizeEnd(win) {
if (win == window) {
this._customizing = false;
this.checkForAutoHide();
DownloadsIndicatorView.afterCustomize();
}
},
init() {
XPCOMUtils.defineLazyPreferenceGetter(
this, "autoHideDownloadsButton", "browser.download.autohideButton",
true, this.checkForAutoHide.bind(this));
CustomizableUI.addListener(this);
this.checkForAutoHide();
},
uninit() {
CustomizableUI.removeListener(this);
},
get _tabsToolbar() {
delete this._tabsToolbar;
return this._tabsToolbar = document.getElementById("TabsToolbar");
@ -342,7 +385,7 @@ const DownloadsIndicatorView = {
let anchor = DownloadsButton._placeholder;
let widgetGroup = CustomizableUI.getWidget("downloads-button");
let widget = widgetGroup.forWindow(window);
if (widget.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
if (anchor && this._isAncestorPanelOpen(anchor)) {
// If the containing panel is open, don't do anything, because the
// notification would appear under the open panel. See
@ -423,8 +466,12 @@ const DownloadsIndicatorView = {
this._hasDownloads = aValue;
// If there is at least one download, ensure that the view elements are
// operational
if (aValue) {
DownloadsButton.unhide();
this._ensureOperational();
} else {
DownloadsButton.checkForAutoHide();
}
}
return aValue;
@ -508,14 +555,7 @@ const DownloadsIndicatorView = {
return;
}
// If the downloads button is in the menu panel, open the Library
let widgetGroup = CustomizableUI.getWidget("downloads-button");
if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
DownloadsPanel.showDownloadsHistory();
} else {
DownloadsPanel.showPanel();
}
DownloadsPanel.showPanel();
aEvent.stopPropagation();
},
@ -567,10 +607,9 @@ const DownloadsIndicatorView = {
},
get indicatorAnchor() {
let widget = CustomizableUI.getWidget("downloads-button")
.forWindow(window);
if (widget.overflowed) {
return widget.anchor;
let widgetGroup = CustomizableUI.getWidget("downloads-button");
if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
return widgetGroup.forWindow(window).anchor;
}
return document.getElementById("downloads-indicator-anchor");
},

View File

@ -13,3 +13,4 @@ skip-if = os == "linux" # Bug 952422
[browser_downloads_panel_block.js]
skip-if = true # Bug 1352792
[browser_downloads_panel_height.js]
[browser_downloads_autohide.js]

View File

@ -0,0 +1,271 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const kDownloadAutoHidePref = "browser.download.autohideButton";
registerCleanupFunction(async function() {
Services.prefs.clearUserPref(kDownloadAutoHidePref);
if (document.documentElement.hasAttribute("customizing")) {
await gCustomizeMode.reset();
await promiseCustomizeEnd();
} else {
CustomizableUI.reset();
}
});
add_task(async function checkStateDuringPrefFlips() {
ok(Services.prefs.getBoolPref(kDownloadAutoHidePref),
"Should be autohiding the button by default");
ok(!DownloadsIndicatorView.hasDownloads,
"Should be no downloads when starting the test");
let downloadsButton = document.getElementById("downloads-button");
ok(downloadsButton.hasAttribute("hidden"),
"Button should be hidden in the toolbar");
gCustomizeMode.addToPanel(downloadsButton);
ok(!downloadsButton.hasAttribute("hidden"),
"Button shouldn't be hidden in the panel");
ok(!Services.prefs.getBoolPref(kDownloadAutoHidePref),
"Pref got set to false when the user moved the button");
gCustomizeMode.addToToolbar(downloadsButton);
ok(!Services.prefs.getBoolPref(kDownloadAutoHidePref),
"Pref remains false when the user moved the button");
Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
ok(downloadsButton.hasAttribute("hidden"),
"Button should be hidden again in the toolbar " +
"now that we flipped the pref");
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
ok(!downloadsButton.hasAttribute("hidden"),
"Button shouldn't be hidden with autohide turned off");
gCustomizeMode.addToPanel(downloadsButton);
ok(!downloadsButton.hasAttribute("hidden"),
"Button shouldn't be hidden with autohide turned off " +
"after moving it to the panel");
gCustomizeMode.addToToolbar(downloadsButton);
ok(!downloadsButton.hasAttribute("hidden"),
"Button shouldn't be hidden with autohide turned off " +
"after moving it back to the toolbar");
gCustomizeMode.addToPanel(downloadsButton);
Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should still not be hidden with autohide turned back on " +
"because it's in the panel");
// Use CUI directly instead of the customize mode APIs,
// to avoid tripping the "automatically turn off autohide" code.
CustomizableUI.addWidgetToArea("downloads-button", "nav-bar");
ok(downloadsButton.hasAttribute("hidden"),
"Button should be hidden again in the toolbar");
gCustomizeMode.removeFromArea(downloadsButton);
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
// Can't use gCustomizeMode.addToToolbar here because it doesn't work for
// palette items if the window isn't in customize mode:
CustomizableUI.addWidgetToArea(downloadsButton.id, CustomizableUI.AREA_NAVBAR);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be unhidden again in the toolbar " +
"even if the pref was flipped while the button was in the palette");
Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
});
add_task(async function checkStateInCustomizeMode() {
ok(Services.prefs.getBoolPref("browser.download.autohideButton"),
"Should be autohiding the button");
let downloadsButton = document.getElementById("downloads-button");
await promiseCustomizeStart();
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in customize mode.");
await promiseCustomizeEnd();
ok(downloadsButton.hasAttribute("hidden"),
"Button should be hidden if it's in the toolbar " +
"after customize mode without any moves.");
await promiseCustomizeStart();
gCustomizeMode.addToPanel(downloadsButton);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in customize mode when moved to the panel");
gCustomizeMode.addToToolbar(downloadsButton);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in customize mode when moved back to the toolbar");
gCustomizeMode.removeFromArea(downloadsButton);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in customize mode when in the palette");
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in customize mode " +
"even when flipping the autohide pref");
gCustomizeMode.addToPanel(downloadsButton);
await promiseCustomizeEnd();
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown after customize mode when moved to the panel");
await promiseCustomizeStart();
gCustomizeMode.addToToolbar(downloadsButton);
await promiseCustomizeEnd();
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in the toolbar after " +
"customize mode because we moved it.");
await promiseCustomizeStart();
await gCustomizeMode.reset();
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in the toolbar in customize mode after a reset.");
await gCustomizeMode.undoReset();
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in the toolbar in customize mode " +
"when undoing the reset.");
gCustomizeMode.addToPanel(downloadsButton);
await gCustomizeMode.reset();
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in the toolbar in customize mode " +
"after a reset moved it.");
await gCustomizeMode.undoReset();
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in the panel in customize mode " +
"when undoing the reset.");
await gCustomizeMode.reset();
await promiseCustomizeEnd();
});
add_task(async function checkStateInCustomizeModeMultipleWindows() {
ok(Services.prefs.getBoolPref("browser.download.autohideButton"),
"Should be autohiding the button");
let downloadsButton = document.getElementById("downloads-button");
await promiseCustomizeStart();
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in customize mode.");
let otherWin = await BrowserTestUtils.openNewBrowserWindow();
let otherDownloadsButton = otherWin.document.getElementById("downloads-button");
ok(otherDownloadsButton.hasAttribute("hidden"),
"Button should be hidden in the other window.");
// Use CUI directly instead of the customize mode APIs,
// to avoid tripping the "automatically turn off autohide" code.
CustomizableUI.addWidgetToArea("downloads-button",
CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should still be shown in customize mode.");
ok(!otherDownloadsButton.hasAttribute("hidden"),
"Button should be shown in the other window too because it's in a panel.");
CustomizableUI.addWidgetToArea("downloads-button", CustomizableUI.AREA_NAVBAR);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should still be shown in customize mode.");
ok(otherDownloadsButton.hasAttribute("hidden"),
"Button should be hidden again in the other window.");
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in customize mode");
ok(!otherDownloadsButton.hasAttribute("hidden"),
"Button should be shown in the other window with the pref flipped");
Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be shown in customize mode " +
"even when flipping the autohide pref");
ok(otherDownloadsButton.hasAttribute("hidden"),
"Button should be hidden in the other window with the pref flipped again");
gCustomizeMode.addToPanel(downloadsButton);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should still be shown in customize mode.");
ok(!otherDownloadsButton.hasAttribute("hidden"),
"Button should be shown in the other window too because it's in a panel.");
gCustomizeMode.removeFromArea(downloadsButton);
ok(!Services.prefs.getBoolPref(kDownloadAutoHidePref),
"Autohide pref turned off by moving the button");
ok(!downloadsButton.hasAttribute("hidden"),
"Button should still be shown in customize mode.");
// Don't need to assert in the other window - button is gone there.
await gCustomizeMode.reset();
ok(Services.prefs.getBoolPref(kDownloadAutoHidePref),
"Autohide pref reset by reset()");
ok(!downloadsButton.hasAttribute("hidden"),
"Button should still be shown in customize mode.");
ok(otherDownloadsButton.hasAttribute("hidden"),
"Button should be hidden in the other window.");
ok(otherDownloadsButton.closest("#nav-bar"),
"Button should be back in the nav bar in the other window.");
await promiseCustomizeEnd();
ok(downloadsButton.hasAttribute("hidden"),
"Button should be hidden again outside of customize mode");
await BrowserTestUtils.closeWindow(otherWin);
});
add_task(async function checkStateForDownloads() {
ok(Services.prefs.getBoolPref("browser.download.autohideButton"),
"Should be autohiding the button");
let downloadsButton = document.getElementById("downloads-button");
ok(downloadsButton.hasAttribute("hidden"),
"Button should be hidden when there are no downloads.");
await task_addDownloads([
{ state: DownloadsCommon.DOWNLOAD_DOWNLOADING },
]);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be unhidden when there are downloads.");
let publicList = await Downloads.getList(Downloads.PUBLIC);
let downloads = await publicList.getAll();
for (let download of downloads) {
publicList.remove(download);
}
ok(downloadsButton.hasAttribute("hidden"),
"Button should be hidden when the download is removed");
await task_addDownloads([
{ state: DownloadsCommon.DOWNLOAD_DOWNLOADING },
]);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should be unhidden when there are downloads.");
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should still be unhidden.");
downloads = await publicList.getAll();
for (let download of downloads) {
publicList.remove(download);
}
ok(!downloadsButton.hasAttribute("hidden"),
"Button should still be unhidden because the pref was flipped.");
Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
ok(downloadsButton.hasAttribute("hidden"),
"Button should be hidden now that the pref flipped back " +
"because there were already no downloads.");
gCustomizeMode.addToPanel(downloadsButton);
ok(!downloadsButton.hasAttribute("hidden"),
"Button should not be hidden in the panel.");
await task_addDownloads([
{ state: DownloadsCommon.DOWNLOAD_DOWNLOADING },
]);
downloads = await publicList.getAll();
for (let download of downloads) {
publicList.remove(download);
}
ok(!downloadsButton.hasAttribute("hidden"),
"Button should still not be hidden in the panel " +
"when downloads count reaches 0 after being non-0.");
});
function promiseCustomizeStart(aWindow = window) {
return new Promise(resolve => {
aWindow.gNavToolbox.addEventListener("customizationready", resolve,
{once: true});
aWindow.gCustomizeMode.enter();
});
}
function promiseCustomizeEnd(aWindow = window) {
return new Promise(resolve => {
aWindow.gNavToolbox.addEventListener("aftercustomization", resolve,
{once: true});
aWindow.gCustomizeMode.exit();
});
}

View File

@ -9,6 +9,8 @@
* updated correctly if downloads are removed while the panel is hidden.
*/
add_task(async function test_height_reduced_after_removal() {
await SpecialPowers.pushPrefEnv({set: [["browser.download.autohideButton", false]]});
await promiseButtonShown("downloads-button");
await task_addDownloads([
{ state: DownloadsCommon.DOWNLOAD_FINISHED },
]);

View File

@ -10,6 +10,8 @@
* not open the panel automatically.
*/
add_task(async function test_first_download_panel() {
await SpecialPowers.pushPrefEnv({set: [["browser.download.autohideButton", false]]});
await promiseButtonShown("downloads-button");
// Clear the download panel has shown preference first as this test is used to
// verify this preference's behaviour.
let oldPrefValue = Services.prefs.getBoolPref("browser.download.panel.shown");
@ -32,6 +34,7 @@ add_task(async function test_first_download_panel() {
// time a download is started.
DownloadsCommon.getData(window).panelHasShownBefore = false;
info("waiting for panel open");
let promise = promisePanelOpened();
DownloadsCommon.getData(window)._notifyDownloadEvent("start");
await promise;

View File

@ -12,8 +12,10 @@ registerCleanupFunction(async function() {
});
add_task(async function test_indicatorDrop() {
await SpecialPowers.pushPrefEnv({set: [["browser.download.autohideButton", false]]});
let downloadButton = document.getElementById("downloads-button");
ok(downloadButton, "download button present");
await promiseButtonShown(downloadButton.id);
let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
getService(Ci.mozIJSSubScriptLoader);

View File

@ -15,30 +15,21 @@ registerCleanupFunction(async function() {
* panel that the downloads panel anchors to the chevron.
*/
add_task(async function test_overflow_anchor() {
await SpecialPowers.pushPrefEnv({set: [["browser.download.autohideButton", false]]});
// Ensure that state is reset in case previous tests didn't finish.
await task_resetState();
// Record the original width of the window so we can put it back when
// this test finishes.
let oldWidth = window.outerWidth;
// The downloads button should not be overflowed to begin with.
let button = CustomizableUI.getWidget("downloads-button")
.forWindow(window);
ok(!button.overflowed, "Downloads button should not be overflowed.");
is(button.node.getAttribute("cui-areatype"), "toolbar", "Button should know it's in the toolbar");
// Hack - we lock the size of the default flex-y items in the nav-bar, namely,
// the URL input. That way we can resize the window without worrying about it
// flexing.
const kFlexyItems = ["urlbar-container"];
registerCleanupFunction(() => unlockWidth(kFlexyItems));
lockWidth(kFlexyItems);
window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
await waitForOverflowed(button, true);
gCustomizeMode.addToPanel(button.node);
let promise = promisePanelOpened();
EventUtils.sendMouseEvent({ type: "mousedown", button: 0 }, button.node);
info("waiting for panel to open");
await promise;
let panel = DownloadsPanel.panel;
@ -47,14 +38,7 @@ add_task(async function test_overflow_anchor() {
DownloadsPanel.hidePanel();
// Unlock the widths on the flex-y items.
unlockWidth(kFlexyItems);
// Put the window back to its original dimensions.
window.resizeTo(oldWidth, window.outerHeight);
// The downloads button should eventually be un-overflowed.
await waitForOverflowed(button, false);
gCustomizeMode.addToToolbar(button.node);
// Now try opening the panel again.
promise = promisePanelOpened();
@ -66,50 +50,3 @@ add_task(async function test_overflow_anchor() {
DownloadsPanel.hidePanel();
});
/**
* For some node IDs, finds the nodes and sets their min-width's to their
* current width, preventing them from flex-shrinking.
*
* @param aItemIDs an array of item IDs to set min-width on.
*/
function lockWidth(aItemIDs) {
for (let itemID of aItemIDs) {
let item = document.getElementById(itemID);
let curWidth = item.getBoundingClientRect().width + "px";
item.style.minWidth = curWidth;
}
}
/**
* Clears the min-width's set on a set of IDs by lockWidth.
*
* @param aItemIDs an array of ItemIDs to remove min-width on.
*/
function unlockWidth(aItemIDs) {
for (let itemID of aItemIDs) {
let item = document.getElementById(itemID);
item.style.minWidth = "";
}
}
/**
* Waits for a node to enter or exit the overflowed state.
*
* @param aItem the node to wait for.
* @param aIsOverflowed if we're waiting for the item to be overflowed.
*/
function waitForOverflowed(aItem, aIsOverflowed) {
if (aItem.overflowed == aIsOverflowed) {
return Promise.resolve();
}
return new Promise(resolve => {
let observer = new MutationObserver(function(aMutations) {
if (aItem.overflowed == aIsOverflowed) {
observer.disconnect();
resolve();
}
});
observer.observe(aItem.node, {attributes: true});
});
}

View File

@ -198,3 +198,15 @@ function promiseAlertDialogOpen(buttonAction) {
});
});
}
/**
* Waits for a given button to become visible.
*/
function promiseButtonShown(id) {
let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
return BrowserTestUtils.waitForCondition(() => {
let target = document.getElementById(id);
let bounds = dwu.getBoundsWithoutFlushing(target);
return bounds.width > 0 && bounds.height > 0;
}, `Waiting for button ${id} to have non-0 size`);
}

View File

@ -45,6 +45,7 @@ skip-if = debug && (os == 'linux' && bits == 32) # Bug 1313372
[browser_ext_browserAction_popup_preload.js]
skip-if = (os == 'win' && !debug) # bug 1352668
[browser_ext_browserAction_popup_resize.js]
skip-if = os == 'mac' # Bug 1374749 will re-enable this test again.
[browser_ext_browserAction_simple.js]
[browser_ext_browserAction_telemetry.js]
[browser_ext_browserAction_theme_icons.js]

View File

@ -126,13 +126,21 @@ FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileD
};
};
function savePrefs() {
// If we've used the pref service to write prefs for the new profile, it's too
// early in startup for the service to have a profile directory, so we have to
// manually tell it where to save the prefs file.
let newPrefsFile = currentProfileDir.clone();
newPrefsFile.append("prefs.js");
Services.prefs.savePrefFile(newPrefsFile);
}
let types = MigrationUtils.resourceTypes;
let places = getFileResource(types.HISTORY, ["places.sqlite", "places.sqlite-wal"]);
let favicons = getFileResource(types.HISTORY, ["favicons.sqlite", "favicons.sqlite-wal"]);
let cookies = getFileResource(types.COOKIES, ["cookies.sqlite", "cookies.sqlite-wal"]);
let passwords = getFileResource(types.PASSWORDS,
["signons.sqlite", "logins.json", "key3.db", "key4.db",
"signedInUser.json"]);
["signons.sqlite", "logins.json", "key3.db", "key4.db"]);
let formData = getFileResource(types.FORMDATA, [
"formhistory.sqlite",
"autofill-profiles.json",
@ -168,11 +176,7 @@ FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileD
// session with the "what's new" page:
Services.prefs.setCharPref("browser.startup.homepage_override.mstone", mstone);
Services.prefs.setCharPref("browser.startup.homepage_override.buildID", buildID);
// It's too early in startup for the pref service to have a profile directory,
// so we have to manually tell it where to save the prefs file.
let newPrefsFile = currentProfileDir.clone();
newPrefsFile.append("prefs.js");
Services.prefs.savePrefFile(newPrefsFile);
savePrefs();
aCallback(true);
}, function() {
aCallback(false);
@ -182,6 +186,37 @@ FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileD
}
}
// Sync/FxA related data
let sync = {
name: "sync", // name is used only by tests.
type: types.OTHERDATA,
migrate: async aCallback => {
// Try and parse a signedInUser.json file from the source directory and
// if we can, copy it to the new profile and set sync's username pref
// (which acts as a de-facto flag to indicate if sync is configured)
try {
let oldPath = OS.Path.join(sourceProfileDir.path, "signedInUser.json");
let exists = await OS.File.exists(oldPath);
if (exists) {
let raw = await OS.File.read(oldPath, {encoding: "utf-8"});
let data = JSON.parse(raw);
if (data && data.accountData && data.accountData.email) {
let username = data.accountData.email;
// Write it to prefs.js and flush the file.
Services.prefs.setStringPref("services.sync.username", username);
savePrefs();
// and copy the file itself.
await OS.File.copy(oldPath, OS.Path.join(currentProfileDir.path, "signedInUser.json"));
}
}
} catch (ex) {
aCallback(false);
return;
}
aCallback(true);
}
};
// Telemetry related migrations.
let times = {
name: "times", // name is used only by tests.
@ -252,7 +287,7 @@ FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileD
};
return [places, cookies, passwords, formData, dictionary, bookmarksBackups,
session, times, telemetry, favicons].filter(r => r);
session, sync, times, telemetry, favicons].filter(r => r);
};
Object.defineProperty(FirefoxProfileMigrator.prototype, "startupOnlyMigrator", {

View File

@ -171,6 +171,17 @@ class TestFirefoxRefresh(MarionetteTestCase):
}
""", script_args=(self._expectedURLs,))
def createSync(self):
# This script will write an entry to the login manager and create
# a signedInUser.json in the profile dir.
self.runAsyncCode("""
Cu.import("resource://gre/modules/FxAccountsStorage.jsm");
let storage = new FxAccountsStorageManager();
let data = {email: "test@test.com", uid: "uid", keyFetchToken: "top-secret"};
storage.initialize(data);
storage.finalize().then(marionetteScriptFinished);
""");
def checkPassword(self):
loginInfo = self.marionette.execute_script("""
let ary = Services.logins.findLogins({},
@ -186,7 +197,8 @@ class TestFirefoxRefresh(MarionetteTestCase):
loginCount = self.marionette.execute_script("""
return Services.logins.getAllLogins().length;
""")
self.assertEqual(loginCount, 1, "No other logins are present")
# Note that we expect 2 logins - one from us, one from sync.
self.assertEqual(loginCount, 2, "No other logins are present")
def checkBookmark(self):
titleInBookmarks = self.marionette.execute_script("""
@ -336,6 +348,34 @@ class TestFirefoxRefresh(MarionetteTestCase):
""")
self.assertSequenceEqual(tabURIs, self._expectedURLs)
def checkSync(self, hasMigrated):
result = self.runAsyncCode("""
Cu.import("resource://gre/modules/FxAccountsStorage.jsm");
let prefs = new global.Preferences("services.sync.");
let storage = new FxAccountsStorageManager();
let result = {};
storage.initialize();
storage.getAccountData().then(data => {
result.accountData = data;
return storage.finalize();
}).then(() => {
result.prefUsername = prefs.get("username");
marionetteScriptFinished(result);
}).catch(err => {
marionetteScriptFinished(err.toString());
});
""");
if type(result) != dict:
self.fail(result)
return
self.assertEqual(result["accountData"]["email"], "test@test.com");
self.assertEqual(result["accountData"]["uid"], "uid");
self.assertEqual(result["accountData"]["keyFetchToken"], "top-secret");
if hasMigrated:
# This test doesn't actually configure sync itself, so the username
# pref only exists after migration.
self.assertEqual(result["prefUsername"], "test@test.com");
def checkProfile(self, hasMigrated=False):
self.checkPassword()
self.checkBookmark()
@ -343,6 +383,7 @@ class TestFirefoxRefresh(MarionetteTestCase):
self.checkFormHistory()
self.checkFormAutofill()
self.checkCookie()
self.checkSync(hasMigrated);
if hasMigrated:
self.checkSession()
@ -354,6 +395,7 @@ class TestFirefoxRefresh(MarionetteTestCase):
self.createFormAutofill()
self.createCookie()
self.createSession()
self.createSync()
def setUpScriptData(self):
self.marionette.set_context(self.marionette.CONTEXT_CHROME)

View File

@ -17,16 +17,20 @@ const LOCAL_NEWTAB_URL = "chrome://browser/content/newtab/newTab.xhtml";
const ACTIVITY_STREAM_URL = "resource://activity-stream/data/content/activity-stream.html";
const ACTIVITY_STREAM_PRERENDER_URL = "resource://activity-stream/data/content/activity-stream-prerendered.html";
const ABOUT_URL = "about:newtab";
const IS_MAIN_PROCESS = Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
// Pref that tells if activity stream is enabled
const PREF_ACTIVITY_STREAM_ENABLED = "browser.newtabpage.activity-stream.enabled";
const PREF_ACTIVITY_STREAM_PRERENDER_ENABLED = "browser.newtabpage.activity-stream.prerender";
function AboutNewTabService() {
Services.obs.addObserver(this, "quit-application-granted");
Services.prefs.addObserver(PREF_ACTIVITY_STREAM_ENABLED, this);
Services.prefs.addObserver(PREF_ACTIVITY_STREAM_PRERENDER_ENABLED, this);
this.toggleActivityStream();
if (IS_MAIN_PROCESS) {
AboutNewTab.init();
@ -71,6 +75,7 @@ AboutNewTabService.prototype = {
_newTabURL: ABOUT_URL,
_activityStreamEnabled: false,
_activityStreamPrerender: true,
_overridden: false,
classID: Components.ID("{dfcd2adc-7867-4d3a-ba70-17501f208142}"),
@ -85,13 +90,19 @@ AboutNewTabService.prototype = {
observe(subject, topic, data) {
switch (topic) {
case "nsPref:changed":
if (this.toggleActivityStream()) {
if (data === PREF_ACTIVITY_STREAM_ENABLED) {
if (this.toggleActivityStream()) {
Services.obs.notifyObservers(null, "newtab-url-changed", ABOUT_URL);
}
} else if (data === PREF_ACTIVITY_STREAM_PRERENDER_ENABLED) {
this._activityStreamPrerender = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_PRERENDER_ENABLED);
Services.obs.notifyObservers(null, "newtab-url-changed", ABOUT_URL);
}
break;
case "quit-application-granted":
Services.obs.removeObserver(this, "quit-application-granted");
Services.prefs.removeObserver(PREF_ACTIVITY_STREAM_ENABLED, this);
Services.prefs.removeObserver(PREF_ACTIVITY_STREAM_PRERENDER_ENABLED, this);
if (IS_MAIN_PROCESS) {
AboutNewTab.uninit();
}
@ -139,7 +150,7 @@ AboutNewTabService.prototype = {
*/
get defaultURL() {
if (this.activityStreamEnabled) {
return this.activityStreamURL;
return this.activityStreamPrerender ? this.activityStreamPrerenderURL : this.activityStreamURL;
}
return LOCAL_NEWTAB_URL;
},
@ -172,10 +183,18 @@ AboutNewTabService.prototype = {
return this._activityStreamEnabled;
},
get activityStreamPrerender() {
return this._activityStreamPrerender;
},
get activityStreamURL() {
return ACTIVITY_STREAM_URL;
},
get activityStreamPrerenderURL() {
return ACTIVITY_STREAM_PRERENDER_URL;
},
resetNewTabURL() {
this._overridden = false;
this._newTabURL = ABOUT_URL;

View File

@ -34,11 +34,21 @@ interface nsIAboutNewTabService : nsISupports
*/
readonly attribute bool activityStreamEnabled;
/**
* Returns true if the the prerendering pref for activity stream is true
*/
readonly attribute bool activityStreamPrerender;
/**
* Returns the activity stream resource URL for the newtab page
*/
readonly attribute ACString activityStreamURL;
/**
* Returns the prerendered activity stream resource URL for the newtab page
*/
readonly attribute ACString activityStreamPrerenderURL;
/**
* Resets to the default resource and also resets the
* overridden attribute to false.

View File

@ -8,17 +8,20 @@ const {utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
"@mozilla.org/browser/aboutnewtab-service;1",
"nsIAboutNewTabService");
const DEFAULT_HREF = aboutNewTabService.activityStreamURL;
const ACTIVITY_STREAM_PRERENDER_URL = aboutNewTabService.activityStreamPrerenderURL;
const DEFAULT_CHROME_URL = "chrome://browser/content/newtab/newTab.xhtml";
const DOWNLOADS_URL = "chrome://browser/content/downloads/contentAreaDownloadsView.xul";
const ACTIVITY_STREAM_PREF = "browser.newtabpage.activity-stream.enabled";
const ACTIVITY_STREAM_PRERENDER_PREF = "browser.newtabpage.activity-stream.prerender";
function cleanup() {
Services.prefs.setBoolPref("browser.newtabpage.activity-stream.enabled", false);
Services.prefs.clearUserPref(ACTIVITY_STREAM_PREF);
Services.prefs.clearUserPref(ACTIVITY_STREAM_PRERENDER_PREF);
aboutNewTabService.resetNewTabURL();
}
@ -29,7 +32,7 @@ do_register_cleanup(cleanup);
*/
add_task(async function test_override_activity_stream_disabled() {
let notificationPromise;
Services.prefs.setBoolPref("browser.newtabpage.activity-stream.enabled", false);
Services.prefs.setBoolPref(ACTIVITY_STREAM_PREF, false);
// tests default is the local newtab resource
Assert.equal(aboutNewTabService.defaultURL, DEFAULT_CHROME_URL,
@ -62,15 +65,12 @@ add_task(async function test_override_activity_stream_disabled() {
});
add_task(async function test_override_activity_stream_enabled() {
let notificationPromise;
// change newtab page to activity stream
notificationPromise = nextChangeNotificationPromise("about:newtab");
Services.prefs.setBoolPref("browser.newtabpage.activity-stream.enabled", true);
await notificationPromise;
let activityStreamURL = aboutNewTabService.activityStreamURL;
Assert.equal(aboutNewTabService.defaultURL, activityStreamURL, "Newtab URL should be the default activity stream URL");
let notificationPromise = await setupASPrerendered();
Assert.equal(aboutNewTabService.defaultURL, ACTIVITY_STREAM_PRERENDER_URL, "Newtab URL should be the default activity stream prerendered URL");
Assert.ok(!aboutNewTabService.overridden, "Newtab URL should not be overridden");
Assert.ok(aboutNewTabService.activityStreamEnabled, "Activity Stream should be enabled");
Assert.ok(aboutNewTabService.activityStreamPrerender, "Activity Stream should be prerendered");
// change to local newtab page while activity stream is enabled
notificationPromise = nextChangeNotificationPromise(DEFAULT_CHROME_URL);
@ -86,6 +86,24 @@ add_task(async function test_override_activity_stream_enabled() {
cleanup();
});
add_task(async function test_default_url() {
await setupASPrerendered();
Assert.equal(aboutNewTabService.defaultURL, ACTIVITY_STREAM_PRERENDER_URL,
"Newtab defaultURL initially set to prerendered AS url");
// Change activity-stream.prerendered to false and wait for the required event to fire
const notificationPromise = nextChangeNotificationPromise(
"about:newtab", "a notification occurs after changing prerender pref");
Services.prefs.setBoolPref(ACTIVITY_STREAM_PRERENDER_PREF, false);
await notificationPromise;
Assert.equal(aboutNewTabService.defaultURL, aboutNewTabService.activityStreamURL,
"Newtab defaultURL set to un-prerendered AS url after the pref has been changed");
cleanup();
});
/**
* Tests reponse to updates to prefs
*/
@ -94,7 +112,8 @@ add_task(async function test_updates() {
* Simulates a "cold-boot" situation, with some pref already set before testing a series
* of changes.
*/
Preferences.set("browser.newtabpage.activity-stream.enabled", true);
await setupASPrerendered();
aboutNewTabService.resetNewTabURL(); // need to set manually because pref notifs are off
let notificationPromise;
@ -110,7 +129,7 @@ add_task(async function test_updates() {
"about:newtab", "a notification occurs on reset");
aboutNewTabService.resetNewTabURL();
Assert.ok(aboutNewTabService.activityStreamEnabled, "Activity Stream should be enabled");
Assert.equal(aboutNewTabService.defaultURL, DEFAULT_HREF, "Default URL should be the activity stream page");
Assert.equal(aboutNewTabService.defaultURL, ACTIVITY_STREAM_PRERENDER_URL, "Default URL should be the activity stream page");
await notificationPromise;
// reset twice, only one notification for default URL
@ -131,3 +150,17 @@ function nextChangeNotificationPromise(aNewURL, testMessage) {
}, "newtab-url-changed");
});
}
function setupASPrerendered() {
if (Services.prefs.getBoolPref(ACTIVITY_STREAM_PREF) &&
Services.prefs.getBoolPref(ACTIVITY_STREAM_PRERENDER_PREF)) {
return Promise.resolve();
}
let notificationPromise;
// change newtab page to activity stream
notificationPromise = nextChangeNotificationPromise("about:newtab");
Services.prefs.setBoolPref(ACTIVITY_STREAM_PREF, true);
Services.prefs.setBoolPref(ACTIVITY_STREAM_PRERENDER_PREF, true);
return notificationPromise;
}

View File

@ -1687,7 +1687,7 @@ BrowserGlue.prototype = {
// eslint-disable-next-line complexity
_migrateUI: function BG__migrateUI() {
const UI_VERSION = 53;
const UI_VERSION = 54;
const BROWSER_DOCURL = "chrome://browser/content/browser.xul";
let currentUIVersion;
@ -2078,6 +2078,15 @@ BrowserGlue.prototype = {
}
}
if (currentUIVersion < 54) {
// Migrate browser.onboarding.hidden to browser.onboarding.state.
if (Services.prefs.prefHasUserValue("browser.onboarding.hidden")) {
let state = Services.prefs.getBoolPref("browser.onboarding.hidden") ? "watermark" : "default";
Services.prefs.setStringPref("browser.onboarding.state", state);
Services.prefs.clearUserPref("browser.onboarding.hidden");
}
}
// Update the migration version.
Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
},

View File

@ -39,22 +39,20 @@ var gSearchResultsPane = {
},
/**
* Check that the passed string matches the filter arguments.
* Check that the text content contains the query string.
*
* @param String str
* to search for filter words in.
* @param String filter
* is a string containing all of the words to filter on.
* @param String content
* the text content to be searched
* @param String query
* the query string
* @returns boolean
* true when match in string else false
* true when the text content contains the query string else false
*/
stringMatchesFilters(str, filter) {
if (!filter || !str) {
queryMatchesContent(content, query) {
if (!content || !query) {
return false;
}
let searchStr = str.toLowerCase();
let filterStrings = filter.toLowerCase().split(/\s+/);
return !filterStrings.some(f => searchStr.indexOf(f) == -1);
return content.toLowerCase().includes(query.toLowerCase());
},
categoriesInitialized: false,
@ -350,7 +348,6 @@ var gSearchResultsPane = {
nodeObject.tagName == "description" ||
nodeObject.tagName == "menulist") {
let simpleTextNodes = this.textNodeDescendants(nodeObject);
for (let node of simpleTextNodes) {
let result = this.highlightMatches([node], [node.length], node.textContent.toLowerCase(), searchPhrase);
matchesFound = matchesFound || result;
@ -376,15 +373,15 @@ var gSearchResultsPane = {
let complexTextNodesResult = this.highlightMatches(accessKeyTextNodes, nodeSizes, allNodeText.toLowerCase(), searchPhrase);
// Searching some elements, such as xul:button, have a 'label' attribute that contains the user-visible text.
let labelResult = this.stringMatchesFilters(nodeObject.getAttribute("label"), searchPhrase);
let labelResult = this.queryMatchesContent(nodeObject.getAttribute("label"), searchPhrase);
// Searching some elements, such as xul:label, store their user-visible text in a "value" attribute.
// Value will be skipped for menuitem since value in menuitem could represent index number to distinct each item.
let valueResult = nodeObject.tagName !== "menuitem" ?
this.stringMatchesFilters(nodeObject.getAttribute("value"), searchPhrase) : false;
this.queryMatchesContent(nodeObject.getAttribute("value"), searchPhrase) : false;
// Searching some elements, such as xul:button, buttons to open subdialogs.
let keywordsResult = this.stringMatchesFilters(nodeObject.getAttribute("searchkeywords"), searchPhrase);
let keywordsResult = this.queryMatchesContent(nodeObject.getAttribute("searchkeywords"), searchPhrase);
// Creating tooltips for buttons
if (keywordsResult && (nodeObject.tagName === "button" || nodeObject.tagName == "menulist")) {

View File

@ -581,6 +581,7 @@
&backgroundColor2.label;
&useSystemColors.label;
&underlineLinks.label;
&links;
&linkColor2.label;
&visitedLinkColor2.label;"/>
</hbox>

View File

@ -156,6 +156,7 @@ function gotoPref(aCategory) {
}
category = friendlyPrefCategoryNameToInternalName(category);
if (category != "paneSearchResults") {
gSearchResultsPane.query = null;
gSearchResultsPane.searchInput.value = "";
gSearchResultsPane.getFindSelection(window).removeAllRanges();
gSearchResultsPane.removeAllSearchTooltips();

View File

@ -259,7 +259,7 @@ var gPrivacyPane = {
let pkiBundle = document.getElementById("pkiBundle");
appendSearchKeywords("passwordExceptions", [
bundlePrefs.getString("savedLoginsExceptions_title"),
bundlePrefs.getString("savedLoginsExceptions_desc2"),
bundlePrefs.getString("savedLoginsExceptions_desc3"),
]);
appendSearchKeywords("showPasswords", [
signonBundle.getString("loginsDescriptionAll2"),
@ -959,7 +959,7 @@ var gPrivacyPane = {
prefilledHost: "",
permissionType: "login-saving",
windowTitle: bundlePrefs.getString("savedLoginsExceptions_title"),
introText: bundlePrefs.getString("savedLoginsExceptions_desc2")
introText: bundlePrefs.getString("savedLoginsExceptions_desc3")
};
gSubDialog.open("chrome://browser/content/preferences/permissions.xul",

View File

@ -21,6 +21,6 @@ add_task(async function() {
*/
add_task(async function() {
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
await evaluateSearchResults("set location permissions", "permissionsGroup");
await evaluateSearchResults("location permissions", "permissionsGroup");
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});

View File

@ -14,7 +14,7 @@ add_task(async function() {
*/
add_task(async function() {
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
await evaluateSearchResults("set camera permissions", "permissionsGroup");
await evaluateSearchResults("camera permissions", "permissionsGroup");
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
@ -23,7 +23,7 @@ add_task(async function() {
*/
add_task(async function() {
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
await evaluateSearchResults("set microphone permissions", "permissionsGroup");
await evaluateSearchResults("microphone permissions", "permissionsGroup");
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
@ -32,6 +32,6 @@ add_task(async function() {
*/
add_task(async function() {
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
await evaluateSearchResults("set notifications permissions", "permissionsGroup");
await evaluateSearchResults("notification permissions", "permissionsGroup");
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});

View File

@ -39,6 +39,7 @@ for (const type of [
"NEW_TAB_INIT",
"NEW_TAB_INITIAL_STATE",
"NEW_TAB_LOAD",
"NEW_TAB_REHYDRATED",
"NEW_TAB_STATE_REQUEST",
"NEW_TAB_UNLOAD",
"OPEN_LINK",
@ -61,6 +62,7 @@ for (const type of [
"SECTION_ENABLE",
"SECTION_REGISTER",
"SECTION_UPDATE",
"SECTION_UPDATE_CARD",
"SET_PREF",
"SHOW_FIREFOX_ACCOUNTS",
"SNIPPETS_DATA",

View File

@ -2,7 +2,9 @@
"use strict";
/* istanbul ignore if */
if (typeof Components !== "undefined" && Components.utils) {
// Note: normally we would just feature detect Components.utils here, but
// unfortunately that throws an ugly warning in content if we do.
if (typeof Window === "undefined" && typeof Components !== "undefined" && Components.utils) {
Components.utils.import("resource://gre/modules/Services.jsm");
}

View File

@ -1,32 +1,73 @@
const prefConfig = {
// Prefs listed with "invalidates: true" will prevent the prerendered version
class _PrerenderData {
constructor(options) {
this.initialPrefs = options.initialPrefs;
this.initialSections = options.initialSections;
this._setValidation(options.validation);
}
get validation() {
return this._validation;
}
set validation(value) {
this._setValidation(value);
}
get invalidatingPrefs() {
return this._invalidatingPrefs;
}
// This is needed so we can use it in the constructor
_setValidation(value = []) {
this._validation = value;
this._invalidatingPrefs = value.reduce((result, next) => {
if (typeof next === "string") {
result.push(next);
return result;
} else if (next && next.oneOf) {
return result.concat(next.oneOf);
}
throw new Error("Your validation configuration is not properly configured");
}, []);
}
arePrefsValid(getPref) {
for (const prefs of this.validation) {
// {oneOf: ["foo", "bar"]}
if (prefs && prefs.oneOf && !prefs.oneOf.some(name => getPref(name) === this.initialPrefs[name])) {
return false;
// "foo"
} else if (getPref(prefs) !== this.initialPrefs[prefs]) {
return false;
}
}
return true;
}
}
this.PrerenderData = new _PrerenderData({
initialPrefs: {
"migrationExpired": true,
"showTopSites": true,
"showSearch": true,
"topSitesCount": 6,
"feeds.section.topstories": true,
"feeds.section.highlights": true
},
// Prefs listed as invalidating will prevent the prerendered version
// of AS from being used if their value is something other than what is listed
// here. This is required because some preferences cause the page layout to be
// too different for the prerendered version to be used. Unfortunately, this
// will result in users who have modified some of their preferences not being
// able to get the benefits of prerendering.
"migrationExpired": {value: true},
"showTopSites": {
value: true,
invalidates: true
},
"showSearch": {
value: true,
invalidates: true
},
"topSitesCount": {value: 6},
"feeds.section.topstories": {
value: true,
invalidates: true
}
};
this.PrerenderData = {
invalidatingPrefs: Object.keys(prefConfig).filter(key => prefConfig[key].invalidates),
initialPrefs: Object.keys(prefConfig).reduce((obj, key) => {
obj[key] = prefConfig[key].value;
return obj;
}, {}), // This creates an object of the form {prefName: value}
validation: [
"showTopSites",
"showSearch",
// This means if either of these are set to their default values,
// prerendering can be used.
{oneOf: ["feeds.section.topstories", "feeds.section.highlights"]}
],
initialSections: [
{
enabled: true,
@ -35,8 +76,16 @@ this.PrerenderData = {
order: 1,
title: {id: "header_recommended_by", values: {provider: "Pocket"}},
topics: [{}]
},
{
enabled: true,
id: "highlights",
icon: "highlights",
order: 2,
title: {id: "header_highlights"}
}
]
};
});
this.EXPORTED_SYMBOLS = ["PrerenderData"];
this._PrerenderData = _PrerenderData;
this.EXPORTED_SYMBOLS = ["PrerenderData", "_PrerenderData"];

View File

@ -233,6 +233,19 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
}
return section;
});
case at.SECTION_UPDATE_CARD:
return prevState.map(section => {
if (section && section.id === action.data.id && section.rows) {
const newRows = section.rows.map(card => {
if (card.url === action.data.url) {
return Object.assign({}, card, action.data.options);
}
return card;
});
return Object.assign({}, section, {rows: newRows});
}
return section;
});
case at.PLACES_BOOKMARK_ADDED:
if (!action.data) {
return prevState;

View File

@ -113,7 +113,8 @@
"showTopSites": true,
"showSearch": true,
"topSitesCount": 6,
"feeds.section.topstories": true
"feeds.section.topstories": true,
"feeds.section.highlights": true
}
},
"Dialog": {
@ -137,6 +138,17 @@
{}
],
"initialized": false
},
{
"title": {
"id": "header_highlights"
},
"rows": [],
"order": 2,
"enabled": true,
"id": "highlights",
"icon": "highlights",
"initialized": false
}
]
};

File diff suppressed because one or more lines are too long

View File

@ -94,7 +94,7 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS :
// UNINIT: "UNINIT"
// }
const actionTypes = {};
for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_REGISTER", "SECTION_UPDATE", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
actionTypes[type] = type;
}
@ -583,6 +583,19 @@ function Sections(prevState = INITIAL_STATE.Sections, action) {
}
return section;
});
case at.SECTION_UPDATE_CARD:
return prevState.map(section => {
if (section && section.id === action.data.id && section.rows) {
const newRows = section.rows.map(card => {
if (card.url === action.data.url) {
return Object.assign({}, card, action.data.options);
}
return card;
});
return Object.assign({}, section, { rows: newRows });
}
return section;
});
case at.PLACES_BOOKMARK_ADDED:
if (!action.data) {
return prevState;
@ -657,8 +670,10 @@ module.exports = {
/* istanbul ignore if */
// Note: normally we would just feature detect Components.utils here, but
// unfortunately that throws an ugly warning in content if we do.
if (typeof Components !== "undefined" && Components.utils) {
if (typeof Window === "undefined" && typeof Components !== "undefined" && Components.utils) {
Components.utils.import("resource://gre/modules/Services.jsm");
}
@ -1046,10 +1061,10 @@ module.exports._unconnected = LinkMenu;
const ReactDOM = __webpack_require__(11);
const Base = __webpack_require__(12);
const { Provider } = __webpack_require__(3);
const initStore = __webpack_require__(28);
const initStore = __webpack_require__(29);
const { reducers } = __webpack_require__(6);
const DetectUserSessionStart = __webpack_require__(30);
const { addSnippetsSubscriber } = __webpack_require__(31);
const DetectUserSessionStart = __webpack_require__(31);
const { addSnippetsSubscriber } = __webpack_require__(32);
const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
new DetectUserSessionStart().sendEventOrAddListener();
@ -1092,6 +1107,7 @@ const ManualMigration = __webpack_require__(22);
const PreferencesPane = __webpack_require__(23);
const Sections = __webpack_require__(24);
const { actionTypes: at, actionCreators: ac } = __webpack_require__(0);
const { PrerenderData } = __webpack_require__(28);
// Add the locale data for pluralization and relative-time formatting for now,
// this just uses english locale data. We can make this more sophisticated if
@ -1103,6 +1119,10 @@ function addLocaleDataForReactIntl({ locale, textDirection }) {
}
class Base extends React.Component {
componentWillMount() {
this.sendNewTabRehydrated(this.props.App);
}
componentDidMount() {
// Request state AFTER the first render to ensure we don't cause the
// prerendered DOM to be unmounted. Otherwise, NEW_TAB_STATE_REQUEST is
@ -1117,7 +1137,10 @@ class Base extends React.Component {
document.getElementById("favicon").href += "#";
}, { once: true });
}
componentWillUpdate({ App }) {
this.sendNewTabRehydrated(App);
// Early loads might not have locale yet, so wait until we do
if (App.locale && App.locale !== this.props.App.locale) {
addLocaleDataForReactIntl(App);
@ -1131,11 +1154,25 @@ class Base extends React.Component {
}
}
// The NEW_TAB_REHYDRATED event is used to inform feeds that their
// data has been consumed e.g. for counting the number of tabs that
// have rendered that data.
sendNewTabRehydrated(App) {
if (App && App.initialized && !this.renderNotified) {
this.props.dispatch(ac.SendToMain({ type: at.NEW_TAB_REHYDRATED, data: {} }));
this.renderNotified = true;
}
}
render() {
const props = this.props;
const { locale, strings, initialized } = props.App;
const prefs = props.Prefs.values;
const shouldBeFixedToTop = PrerenderData.arePrefsValid(name => prefs[name]);
const outerClassName = `outer-wrapper${shouldBeFixedToTop ? " fixed-to-top" : ""}`;
if (!props.isPrerendered && !initialized) {
return null;
}
@ -1148,7 +1185,7 @@ class Base extends React.Component {
{ key: "STATIC", locale: locale, messages: strings },
React.createElement(
"div",
{ className: "outer-wrapper" },
{ className: outerClassName },
React.createElement(
"main",
null,
@ -1165,6 +1202,7 @@ class Base extends React.Component {
}
module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base);
module.exports._unconnected = Base;
/***/ }),
/* 13 */
@ -2642,6 +2680,8 @@ class Card extends React.Component {
const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
// Display "now" as "trending" until we have new strings #3402
const { icon, intlID } = cardContextTypes[link.type === "now" ? "trending" : link.type] || {};
const hasImage = link.image || link.hasImage;
const imageStyle = { backgroundImage: link.image ? `url(${link.image})` : "none" };
return React.createElement(
"li",
@ -2652,10 +2692,14 @@ class Card extends React.Component {
React.createElement(
"div",
{ className: "card" },
link.image && React.createElement("div", { className: "card-preview-image", style: { backgroundImage: `url(${link.image})` } }),
hasImage && React.createElement(
"div",
{ className: "card-preview-image-outer" },
React.createElement("div", { className: `card-preview-image${link.image ? " loaded" : ""}`, style: imageStyle })
),
React.createElement(
"div",
{ className: `card-details${link.image ? "" : " no-image"}` },
{ className: `card-details${hasImage ? "" : " no-image"}` },
link.hostname && React.createElement(
"div",
{ className: "card-host-name" },
@ -2663,7 +2707,7 @@ class Card extends React.Component {
),
React.createElement(
"div",
{ className: `card-text${link.image ? "" : " no-image"}${link.hostname ? "" : " no-host-name"}${icon ? "" : " no-context"}` },
{ className: `card-text${hasImage ? "" : " no-image"}${link.hostname ? "" : " no-host-name"}${icon ? "" : " no-context"}` },
React.createElement(
"h4",
{ className: "card-title", dir: "auto" },
@ -2792,11 +2836,102 @@ module.exports.Topic = Topic;
/***/ }),
/* 28 */
/***/ (function(module, exports) {
class _PrerenderData {
constructor(options) {
this.initialPrefs = options.initialPrefs;
this.initialSections = options.initialSections;
this._setValidation(options.validation);
}
get validation() {
return this._validation;
}
set validation(value) {
this._setValidation(value);
}
get invalidatingPrefs() {
return this._invalidatingPrefs;
}
// This is needed so we can use it in the constructor
_setValidation(value = []) {
this._validation = value;
this._invalidatingPrefs = value.reduce((result, next) => {
if (typeof next === "string") {
result.push(next);
return result;
} else if (next && next.oneOf) {
return result.concat(next.oneOf);
}
throw new Error("Your validation configuration is not properly configured");
}, []);
}
arePrefsValid(getPref) {
for (const prefs of this.validation) {
// {oneOf: ["foo", "bar"]}
if (prefs && prefs.oneOf && !prefs.oneOf.some(name => getPref(name) === this.initialPrefs[name])) {
return false;
// "foo"
} else if (getPref(prefs) !== this.initialPrefs[prefs]) {
return false;
}
}
return true;
}
}
var PrerenderData = new _PrerenderData({
initialPrefs: {
"migrationExpired": true,
"showTopSites": true,
"showSearch": true,
"topSitesCount": 6,
"feeds.section.topstories": true,
"feeds.section.highlights": true
},
// Prefs listed as invalidating will prevent the prerendered version
// of AS from being used if their value is something other than what is listed
// here. This is required because some preferences cause the page layout to be
// too different for the prerendered version to be used. Unfortunately, this
// will result in users who have modified some of their preferences not being
// able to get the benefits of prerendering.
validation: ["showTopSites", "showSearch",
// This means if either of these are set to their default values,
// prerendering can be used.
{ oneOf: ["feeds.section.topstories", "feeds.section.highlights"] }],
initialSections: [{
enabled: true,
icon: "pocket",
id: "topstories",
order: 1,
title: { id: "header_recommended_by", values: { provider: "Pocket" } },
topics: [{}]
}, {
enabled: true,
id: "highlights",
icon: "highlights",
order: 2,
title: { id: "header_highlights" }
}]
});
module.exports = {
PrerenderData,
_PrerenderData
};
/***/ }),
/* 29 */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {/* eslint-env mozilla/frame-script */
const { createStore, combineReducers, applyMiddleware } = __webpack_require__(29);
const { createStore, combineReducers, applyMiddleware } = __webpack_require__(30);
const { actionTypes: at, actionCreators: ac, actionUtils: au } = __webpack_require__(0);
const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
@ -2906,13 +3041,13 @@ module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 29 */
/* 30 */
/***/ (function(module, exports) {
module.exports = Redux;
/***/ }),
/* 30 */
/* 31 */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {const { actionTypes: at } = __webpack_require__(0);
@ -2982,7 +3117,7 @@ module.exports = class DetectUserSessionStart {
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
/***/ }),
/* 31 */
/* 32 */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {const DATABASE_NAME = "snippets_db";

View File

@ -168,9 +168,11 @@ a {
.outer-wrapper {
display: flex;
flex-grow: 1;
padding: 40px 32px 32px;
height: 100%; }
height: 100%;
flex-grow: 1; }
.outer-wrapper.fixed-to-top {
height: auto; }
main {
margin: auto;
@ -976,14 +978,30 @@ main {
opacity: 1; }
.card-outer:hover .card-title, .card-outer:focus .card-title, .card-outer.active .card-title {
color: #0060DF; }
.card-outer .card-preview-image {
.card-outer .card-preview-image-outer {
position: relative;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background: linear-gradient(135deg, #B1B1B3, #D7D7DB);
height: 122px;
border-bottom: 1px solid #D7D7DB;
border-radius: 3px 3px 0 0; }
border-radius: 3px 3px 0 0;
overflow: hidden; }
.card-outer .card-preview-image-outer:dir(rtl) {
background: linear-gradient(225deg, #B1B1B3, #D7D7DB); }
.card-outer .card-preview-image-outer::after {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
bottom: 0;
content: " ";
position: absolute;
width: 100%; }
.card-outer .card-preview-image-outer .card-preview-image {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transition: opacity 1s;
opacity: 0; }
.card-outer .card-preview-image-outer .card-preview-image.loaded {
opacity: 1; }
.card-outer .card-details {
padding: 15px 16px 12px; }
.card-outer .card-details.no-image {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -11,33 +11,13 @@
},
{
"title": "amazon",
"url": "https://www.amazon.ca/",
"image_url": "amazon-ca@2x.png"
},
{
"title": "amazon",
"url": "https://www.amazon.co.uk/",
"image_url": "amazon-uk@2x.png"
},
{
"title": "amazon",
"url": "https://www.amazon.com/",
"image_url": "amazon-com@2x.png"
},
{
"title": "amazon",
"url": "https://www.amazon.de/",
"image_url": "amazon-de@2x.png"
},
{
"title": "amazon",
"url": "https://www.amazon.fr/",
"image_url": "amazon-fr@2x.png"
"urls": ["https://www.amazon.ca/", "https://www.amazon.co.uk/", "https://www.amazon.com/", "https://www.amazon.de/", "https://www.amazon.fr/"],
"image_url": "amazon@2x.png"
},
{
"title": "avito",
"url": "https://www.avito.ru/",
"image_url": "avito@2x.png"
"image_url": "avito-ru@2x.png"
},
{
"title": "bbc",
@ -46,14 +26,9 @@
},
{
"title": "ebay",
"urls": ["https://www.ebay.com", "https://www.ebay.co.uk/"],
"urls": ["https://www.ebay.com", "https://www.ebay.co.uk/", "https://ebay.de"],
"image_url": "ebay@2x.png"
},
{
"title": "ebay",
"url": "https://ebay.de",
"image_url": "ebay-de@2x.png"
},
{
"title": "facebook",
"url": "https://www.facebook.com/",
@ -62,12 +37,12 @@
{
"title": "leboncoin",
"url": "http://www.leboncoin.fr/",
"image_url": "leboncoin@2x.png"
"image_url": "leboncoin-fr@2x.png"
},
{
"title": "ok",
"url": "https://www.ok.ru/",
"image_url": "ok@2x.png"
"image_url": "ok-ru@2x.png"
},
{
"title": "olx",
@ -92,12 +67,12 @@
{
"title": "wikipedia",
"url": "https://www.wikipedia.org/",
"image_url": "wikipedia-com@2x.png"
"image_url": "wikipedia-org@2x.png"
},
{
"title": "wykop",
"url": "https://www.wykop.pl/",
"image_url": "wykop@2x.png"
"image_url": "wykop-pl@2x.png"
},
{
"title": "youtube",

View File

@ -5997,8 +5997,8 @@
"header_recommended_by": "{provider} öneriyor",
"header_bookmarks_placeholder": "Henüz hiç yer iminiz yok.",
"header_stories_from": "kaynak:",
"type_label_visited": "Ziyaret edildi",
"type_label_bookmarked": "Yer imlerine eklendi",
"type_label_visited": "Ziyaret etmiştiniz",
"type_label_bookmarked": "Yer imlerinizde",
"type_label_synced": "Başka bir cihazdan eşitlendi",
"type_label_recommended": "Popüler",
"type_label_open": "Açık",
@ -6075,7 +6075,7 @@
"pocket_description": "Mozilla ailesinin yeni üyesi Pocketın yardımıyla, gözünüzden kaçabilecek kaliteli içerikleri keşfedin.",
"highlights_empty_state": "Gezinmeye başlayın. Son zamanlarda baktığınız veya yer imlerinize eklediğiniz bazı güzel makaleleri, videoları ve diğer sayfaları burada göstereceğiz.",
"topstories_empty_state": "Hepsini bitirdiniz. Yeni {provider} haberleri için daha fazla yine gelin. Beklemek istemiyor musunuz? İlginç yazılara ulaşmak için popüler konulardan birini seçebilirsiniz.",
"manual_migration_explanation2": "Öteki tarayıcılarınızdaki yer işaretlerinizi, geçmişinizi ve parolalarınızı Firefoxa taşıyabilirsiniz.",
"manual_migration_explanation2": "Öteki tarayıcılarınızdaki yer imlerinizi, geçmişinizi ve parolalarınızı Firefoxa aktarabilirsiniz.",
"manual_migration_cancel_button": "Gerek yok",
"manual_migration_import_button": "Olur, aktaralım"
},

View File

@ -8,7 +8,7 @@
<em:type>2</em:type>
<em:bootstrap>true</em:bootstrap>
<em:unpack>false</em:unpack>
<em:version>2017.09.11.1306-373d9fc</em:version>
<em:version>2017.09.12.1376-781e5de5</em:version>
<em:name>Activity Stream</em:name>
<em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
<em:multiprocessCompatible>true</em:multiprocessCompatible>

View File

@ -60,7 +60,8 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
*/
middleware(store) {
return next => action => {
if (!this.channel) {
const skipMain = action.meta && action.meta.skipMain;
if (!this.channel && !skipMain) {
next(action);
return;
}
@ -69,7 +70,10 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
} else if (au.isBroadcastToContent(action)) {
this.broadcast(action);
}
next(action);
if (!skipMain) {
next(action);
}
};
}
@ -136,10 +140,10 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
this.channel.addMessageListener(this.incomingMessageName, this.onMessage);
// Some pages might have already loaded, so we won't get the usual message
for (const {loaded, portID} of this.channel.messagePorts) {
const simulatedMsg = {target: {portID}};
for (const target of this.channel.messagePorts) {
const simulatedMsg = {target};
this.onNewTabInit(simulatedMsg);
if (loaded) {
if (target.loaded) {
this.onNewTabLoad(simulatedMsg);
}
}
@ -168,7 +172,10 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
* @param {obj} msg The messsage from a page that was just initialized
*/
onNewTabInit(msg) {
this.onActionFromContent({type: at.NEW_TAB_INIT}, msg.target.portID);
this.onActionFromContent({
type: at.NEW_TAB_INIT,
data: {url: msg.target.url}
}, msg.target.portID);
}
/**

View File

@ -15,6 +15,8 @@ const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Screenshots",
"resource://activity-stream/lib/Screenshots.jsm");
const HIGHLIGHTS_MAX_LENGTH = 9;
const HIGHLIGHTS_UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
@ -26,6 +28,7 @@ this.HighlightsFeed = class HighlightsFeed {
this.highlightsLastUpdated = 0;
this.highlights = [];
this.dedupe = new Dedupe(this._dedupeKey);
this.imageCache = new Map();
}
_dedupeKey(site) {
@ -62,10 +65,16 @@ this.HighlightsFeed = class HighlightsFeed {
continue;
}
// If we already have the image for the card in the cache, use that
// immediately. Then asynchronously fetch the image (refreshes the cache).
const image = this.imageCache.get(page.url);
this.fetchImage(page.url, page.preview_image_url);
// We want the page, so update various fields for UI
Object.assign(page, {
image,
hasImage: true, // We always have an image - fall back to a screenshot
hostname,
image: page.preview_image_url,
type: page.bookmarkGuid ? "bookmark" : page.type
});
@ -81,6 +90,23 @@ this.HighlightsFeed = class HighlightsFeed {
SectionsManager.updateSection(SECTION_ID, {rows: this.highlights}, this.highlightsLastUpdated === 0 || broadcast);
this.highlightsLastUpdated = Date.now();
// Clearing the image cache here is okay. The asynchronous fetchImage calls
// get scheduled after the body of fetchHighlights has been executed, so they
// then fill up the cache again for the next fetchHighlights call.
this.imageCache.clear();
}
/**
* Fetch an image for a given highlight, store it in the image cache, and
* update the card with the new image. If the highlight has a preview image
* then use that, else fall back to a screenshot of the page.
*/
async fetchImage(url, imageUrl) {
const image = await Screenshots.getScreenshotForURL(imageUrl || url);
if (image) {
this.imageCache.set(url, image);
}
SectionsManager.updateSectionCard(SECTION_ID, url, {image}, true);
}
onAction(action) {

View File

@ -17,14 +17,8 @@ this.PrefsFeed = class PrefsFeed {
// If the any prefs are set to something other than what the prerendered version
// of AS expects, we can't use it.
_setPrerenderPref() {
for (const prefName of PrerenderData.invalidatingPrefs) {
if (this._prefs.get(prefName) !== PrerenderData.initialPrefs[prefName]) {
this._prefs.set("prerender", false);
return;
}
}
this._prefs.set("prerender", true);
_setPrerenderPref(name) {
this._prefs.set("prerender", PrerenderData.arePrefsValid(pref => this._prefs.get(pref)));
}
_checkPrerender(name) {
@ -37,7 +31,7 @@ this.PrefsFeed = class PrefsFeed {
if (this._prefMap.has(name)) {
this.store.dispatch(ac.BroadcastToContent({type: at.PREF_CHANGED, data: {name, value}}));
}
this._checkPrerender(name, value);
this._checkPrerender(name);
}
init() {

View File

@ -19,6 +19,8 @@ XPCOMUtils.defineLazyServiceGetter(this, "MIMEService",
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
const GREY_10 = "#F9F9FA";
this.Screenshots = {
/**
* Convert bytes to a string using extremely fast String.fromCharCode without
@ -41,7 +43,7 @@ this.Screenshots = {
async getScreenshotForURL(url) {
let screenshot = null;
try {
await BackgroundPageThumbs.captureIfMissing(url);
await BackgroundPageThumbs.captureIfMissing(url, {backgroundColor: GREY_10});
const imgPath = PageThumbs.getThumbnailPath(url);
// OS.File object used to easily read off-thread

View File

@ -139,6 +139,26 @@ const SectionsManager = {
o => !this.CONTEXT_MENU_PREFS[o] || Services.prefs.getBoolPref(this.CONTEXT_MENU_PREFS[o]));
}
},
/**
* Update a specific section card by its url. This allows an action to be
* broadcast to all existing pages to update a specific card without having to
* also force-update the rest of the section's cards and state on those pages.
*
* @param id The id of the section with the card to be updated
* @param url The url of the card to update
* @param options The options to update for the card
* @param shouldBroadcast Whether or not to broadcast the update
*/
updateSectionCard(id, url, options, shouldBroadcast) {
if (this.sections.has(id)) {
const card = this.sections.get(id).rows.find(elem => elem.url === url);
if (card) {
Object.assign(card, options);
}
this.emit(this.UPDATE_SECTION_CARD, id, url, options, shouldBroadcast);
}
},
onceInitialized(callback) {
if (this.initialized) {
callback();
@ -160,6 +180,7 @@ for (const action of [
"ENABLE_SECTION",
"DISABLE_SECTION",
"UPDATE_SECTION",
"UPDATE_SECTION_CARD",
"INIT",
"UNINIT"
]) {
@ -174,12 +195,14 @@ class SectionsFeed {
this.onAddSection = this.onAddSection.bind(this);
this.onRemoveSection = this.onRemoveSection.bind(this);
this.onUpdateSection = this.onUpdateSection.bind(this);
this.onUpdateSectionCard = this.onUpdateSectionCard.bind(this);
}
init() {
SectionsManager.on(SectionsManager.ADD_SECTION, this.onAddSection);
SectionsManager.on(SectionsManager.REMOVE_SECTION, this.onRemoveSection);
SectionsManager.on(SectionsManager.UPDATE_SECTION, this.onUpdateSection);
SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, this.onUpdateSectionCard);
// Catch any sections that have already been added
SectionsManager.sections.forEach((section, id) =>
this.onAddSection(SectionsManager.ADD_SECTION, id, section));
@ -191,6 +214,7 @@ class SectionsFeed {
SectionsManager.off(SectionsManager.ADD_SECTION, this.onAddSection);
SectionsManager.off(SectionsManager.REMOVE_SECTION, this.onRemoveSection);
SectionsManager.off(SectionsManager.UPDATE_SECTION, this.onUpdateSection);
SectionsManager.off(SectionsManager.UPDATE_SECTION_CARD, this.onUpdateSectionCard);
}
onAddSection(event, id, options) {
@ -210,6 +234,13 @@ class SectionsFeed {
}
}
onUpdateSectionCard(event, id, url, options, shouldBroadcast = false) {
if (options) {
const action = {type: at.SECTION_UPDATE_CARD, data: {id, url, options}};
this.store.dispatch(shouldBroadcast ? ac.BroadcastToContent(action) : action);
}
}
onAction(action) {
switch (action.type) {
case at.INIT:

View File

@ -189,17 +189,21 @@ this.TelemetryFeed = class TelemetryFeed {
* addSession - Start tracking a new session
*
* @param {string} id the portID of the open session
*
* @param {string} the URL being loaded for this session (optional)
* @return {obj} Session object
*/
addSession(id) {
addSession(id, url) {
const session = {
session_id: String(gUUIDGenerator.generateUUID()),
page: "about:newtab", // TODO: Handle about:home here
// "unknown" will be overwritten when appropriate
page: url ? url : "unknown",
// "unexpected" will be overwritten when appropriate
perf: {load_trigger_type: "unexpected"}
};
if (url) {
session.page = url;
}
this.sessions.set(id, session);
return session;
}
@ -242,10 +246,11 @@ this.TelemetryFeed = class TelemetryFeed {
// If the ping is part of a user session, add session-related info
if (portID) {
const session = this.sessions.get(portID) || this.addSession(portID);
Object.assign(ping, {
session_id: session.session_id,
page: session.page
});
Object.assign(ping, {session_id: session.session_id});
if (session.page) {
Object.assign(ping, {page: session.page});
}
}
return ping;
}
@ -355,7 +360,7 @@ this.TelemetryFeed = class TelemetryFeed {
this.init();
break;
case at.NEW_TAB_INIT:
this.addSession(au.getPortIdOfSender(action));
this.addSession(au.getPortIdOfSender(action), action.data.url);
break;
case at.NEW_TAB_UNLOAD:
this.endSession(au.getPortIdOfSender(action));

View File

@ -57,7 +57,7 @@ this.TopSitesFeed = class TopSitesFeed {
let frecent = await NewTabUtils.activityStreamLinks.getTopSites();
const notBlockedDefaultSites = DEFAULT_TOP_SITES.filter(site => !NewTabUtils.blockedLinks.isBlocked({url: site.url}));
const defaultUrls = notBlockedDefaultSites.map(site => site.url);
let pinned = NewTabUtils.pinnedLinks.links;
let pinned = this._getPinnedWithData(frecent);
pinned = pinned.map(site => site && Object.assign({}, site, {
isDefault: defaultUrls.indexOf(site.url) !== -1,
hostname: shortURL(site)
@ -122,14 +122,18 @@ this.TopSitesFeed = class TopSitesFeed {
}
this.lastUpdated = Date.now();
}
_getPinnedWithData() {
// Augment the pinned links with any other extra data we have for them already in the store
const links = this.store.getState().TopSites.rows;
_getPinnedWithData(links) {
// Augment the pinned links with any other extra data we have for them already in the store.
// Alternatively you can pass in some links that you know have data you want the pinned links
// to also have. This is useful for start up to make sure pinned links have favicons
// (See github ticket #3428 fore more details)
let originalLinks = links ? links : this.store.getState().TopSites.rows;
const pinned = NewTabUtils.pinnedLinks.links;
return pinned.map(pinnedLink => {
if (pinnedLink) {
const hostname = shortURL(pinnedLink);
return Object.assign(links.find(link => link && link.url === pinnedLink.url) || {hostname}, pinnedLink);
const originalLink = originalLinks.find(link => link && link.url === pinnedLink.url);
return Object.assign(pinnedLink, originalLink || {hostname});
}
return pinnedLink;
});

View File

@ -7,7 +7,7 @@ const baseKeys = {
addon_version: Joi.string().required(),
locale: Joi.string().required(),
session_id: Joi.string(),
page: Joi.valid(["about:home", "about:newtab"]),
page: Joi.valid(["about:home", "about:newtab", "unknown"]),
user_prefs: Joi.number().integer().required()
};

View File

@ -0,0 +1,98 @@
const {PrerenderData, _PrerenderData} = require("common/PrerenderData.jsm");
describe("_PrerenderData", () => {
describe("properties", () => {
it("should set .initialPrefs", () => {
const initialPrefs = {foo: true};
const instance = new _PrerenderData({initialPrefs});
assert.equal(instance.initialPrefs, initialPrefs);
});
it("should set .initialSections", () => {
const initialSections = [{id: "foo"}];
const instance = new _PrerenderData({initialSections});
assert.equal(instance.initialSections, initialSections);
});
it("should set .validation and .invalidatingPrefs in the constructor", () => {
const validation = ["foo", "bar", {oneOf: ["baz", "qux"]}];
const instance = new _PrerenderData({validation});
assert.equal(instance.validation, validation);
assert.deepEqual(instance.invalidatingPrefs, ["foo", "bar", "baz", "qux"]);
});
it("should also set .invalidatingPrefs when .validation is set", () => {
const validation = ["foo", "bar", {oneOf: ["baz", "qux"]}];
const instance = new _PrerenderData({validation});
const newValidation = ["foo", {oneOf: ["blah", "gloo"]}];
instance.validation = newValidation;
assert.equal(instance.validation, newValidation);
assert.deepEqual(instance.invalidatingPrefs, ["foo", "blah", "gloo"]);
});
it("should throw if an invalid validation config is set", () => {
// {stuff: []} is not a valid configuration type
assert.throws(() => new _PrerenderData({validation: ["foo", {stuff: ["bar"]}]}));
});
});
describe("#arePrefsValid", () => {
let FAKE_PREFS;
const getPrefs = pref => FAKE_PREFS[pref];
beforeEach(() => {
FAKE_PREFS = {};
});
it("should return true if all prefs match", () => {
FAKE_PREFS = {foo: true, bar: false};
const instance = new _PrerenderData({
initialPrefs: FAKE_PREFS,
validation: ["foo", "bar"]
});
assert.isTrue(instance.arePrefsValid(getPrefs));
});
it("should return true if all *invalidating* prefs match", () => {
FAKE_PREFS = {foo: true, bar: false};
const instance = new _PrerenderData({
initialPrefs: {foo: true, bar: true},
validation: ["foo"]
});
assert.isTrue(instance.arePrefsValid(getPrefs));
});
it("should return true if one each oneOf group matches", () => {
FAKE_PREFS = {foo: false, floo: true, bar: false, blar: true};
const instance = new _PrerenderData({
initialPrefs: {foo: true, floo: true, bar: true, blar: true},
validation: [{oneOf: ["foo", "floo"]}, {oneOf: ["bar", "blar"]}]
});
assert.isTrue(instance.arePrefsValid(getPrefs));
});
it("should return false if an invalidating pref is mismatched", () => {
FAKE_PREFS = {foo: true, bar: false};
const instance = new _PrerenderData({
initialPrefs: {foo: true, bar: true},
validation: ["foo", "bar"]
});
assert.isFalse(instance.arePrefsValid(getPrefs));
});
it("should return false if none of the oneOf group matches", () => {
FAKE_PREFS = {foo: true, bar: false, baz: false};
const instance = new _PrerenderData({
initialPrefs: {foo: true, bar: true, baz: true},
validation: ["foo", {oneOf: ["bar", "baz"]}]
});
assert.isFalse(instance.arePrefsValid(getPrefs));
});
});
});
// This is the instance used by Activity Stream
describe("PrerenderData", () => {
it("should set initial values for all invalidating prefs", () => {
PrerenderData.invalidatingPrefs.forEach(pref => {
assert.property(PrerenderData.initialPrefs, pref);
});
});
});

View File

@ -328,6 +328,30 @@ describe("Reducers", () => {
const updatedSection = newState.find(section => section.id === "foo_bar_2");
assert.propertyVal(updatedSection, "initialized", true);
});
it("should have no effect on SECTION_UPDATE_CARD if the id or url doesn't exist", () => {
const noIdAction = {type: at.SECTION_UPDATE_CARD, data: {id: "non-existent", url: "www.foo.bar", options: {title: "New title"}}};
const noIdState = Sections(oldState, noIdAction);
const noUrlAction = {type: at.SECTION_UPDATE_CARD, data: {id: "foo_bar_2", url: "www.non-existent.url", options: {title: "New title"}}};
const noUrlState = Sections(oldState, noUrlAction);
assert.deepEqual(noIdState, oldState);
assert.deepEqual(noUrlState, oldState);
});
it("should update the card with the correct data on SECTION_UPDATE_CARD", () => {
const action = {type: at.SECTION_UPDATE_CARD, data: {id: "foo_bar_2", url: "www.other.url", options: {title: "Fake new title"}}};
const newState = Sections(oldState, action);
const updatedSection = newState.find(section => section.id === "foo_bar_2");
const updatedCard = updatedSection.rows.find(card => card.url === "www.other.url");
assert.propertyVal(updatedCard, "title", "Fake new title");
});
it("should only update the cards belonging to the right section on SECTION_UPDATE_CARD", () => {
const action = {type: at.SECTION_UPDATE_CARD, data: {id: "foo_bar_2", url: "www.other.url", options: {title: "Fake new title"}}};
const newState = Sections(oldState, action);
newState.forEach((section, i) => {
if (section.id !== "foo_bar_2") {
assert.deepEqual(section, oldState[i]);
}
});
});
it("should allow action.data to set .initialized", () => {
const data = {rows: [], initialized: false, id: "foo_bar_2"};
const action = {type: at.SECTION_UPDATE, data};

View File

@ -82,13 +82,22 @@ describe("ActivityStreamMessageChannel", () => {
});
it("should simluate init for existing ports", () => {
sinon.stub(mm, "onActionFromContent");
RPmessagePorts.push({loaded: false, portID: "inited"});
RPmessagePorts.push({loaded: true, portID: "loaded"});
RPmessagePorts.push({
url: "about:monkeys",
loaded: false,
portID: "inited"
});
RPmessagePorts.push({
url: "about:sheep",
loaded: true,
portID: "loaded"
});
mm.createChannel();
assert.calledWith(mm.onActionFromContent.firstCall, {type: at.NEW_TAB_INIT}, "inited");
assert.calledWith(mm.onActionFromContent.secondCall, {type: at.NEW_TAB_INIT}, "loaded");
assert.calledWith(mm.onActionFromContent.firstCall, {type: at.NEW_TAB_INIT, data: {url: "about:monkeys"}}, "inited");
assert.calledWith(mm.onActionFromContent.secondCall, {type: at.NEW_TAB_INIT, data: {url: "about:sheep"}}, "loaded");
});
it("should simluate load for loaded ports", () => {
sinon.stub(mm, "onActionFromContent");
@ -146,10 +155,15 @@ describe("ActivityStreamMessageChannel", () => {
});
describe("#onNewTabInit", () => {
it("should dispatch a NEW_TAB_INIT action", () => {
const t = {portID: "foo"};
const t = {portID: "foo", url: "about:monkeys"};
sinon.stub(mm, "onActionFromContent");
mm.onNewTabInit({target: t});
assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_INIT}, "foo");
assert.calledWith(mm.onActionFromContent, {
type: at.NEW_TAB_INIT,
data: {url: "about:monkeys"}
}, "foo");
});
});
describe("#onNewTabLoad", () => {
@ -245,6 +259,17 @@ describe("ActivityStreamMessageChannel", () => {
store.dispatch({type: "ADD", data: 10});
assert.equal(store.getState(), 10);
});
it("should not call next if skipMain is true", () => {
store.dispatch({type: "ADD", data: 10, meta: {skipMain: true}});
assert.equal(store.getState(), 0);
sinon.stub(mm, "send");
const action = ac.SendToContent({type: "ADD", data: 10, meta: {skipMain: true}}, "foo");
mm.createChannel();
store.dispatch(action);
assert.calledWith(mm.send, action);
assert.equal(store.getState(), 0);
});
it("should call .send if the action is SendToContent", () => {
sinon.stub(mm, "send");
const action = ac.SendToContent({type: "FOO"}, "foo");

View File

@ -5,8 +5,9 @@ const {actionTypes: at} = require("common/Actions.jsm");
const {Dedupe} = require("common/Dedupe.jsm");
const FAKE_LINKS = new Array(9).fill(null).map((v, i) => ({url: `http://www.site${i}.com`}));
const FAKE_IMAGE = "data123";
describe("Top Sites Feed", () => {
describe("Highlights Feed", () => {
let HighlightsFeed;
let HIGHLIGHTS_UPDATE_TIME;
let SECTION_ID;
@ -15,6 +16,7 @@ describe("Top Sites Feed", () => {
let sandbox;
let links;
let clock;
let fakeScreenshot;
let fakeNewTabUtils;
let sectionsManagerStub;
let shortURLStub;
@ -28,13 +30,16 @@ describe("Top Sites Feed", () => {
enableSection: sinon.spy(),
disableSection: sinon.spy(),
updateSection: sinon.spy(),
updateSectionCard: sinon.spy(),
sections: new Map([["highlights", {}]])
};
fakeScreenshot = {getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_IMAGE))};
shortURLStub = sinon.stub().callsFake(site => site.url.match(/\/([^/]+)/)[1]);
globals.set("NewTabUtils", fakeNewTabUtils);
({HighlightsFeed, HIGHLIGHTS_UPDATE_TIME, SECTION_ID} = injector({
"lib/ShortURL.jsm": {shortURL: shortURLStub},
"lib/SectionsManager.jsm": {SectionsManager: sectionsManagerStub},
"lib/Screenshots.jsm": {Screenshots: fakeScreenshot},
"common/Dedupe.jsm": {Dedupe}
}));
feed = new HighlightsFeed();
@ -69,11 +74,24 @@ describe("Top Sites Feed", () => {
});
});
describe("#fetchHighlights", () => {
it("should add hostname and image to each link", async () => {
links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
it("should add hostname and hasImage to each link", async () => {
links = [{url: "https://mozilla.org"}];
await feed.fetchHighlights();
assert.equal(feed.highlights[0].hostname, "mozilla.org");
assert.equal(feed.highlights[0].image, links[0].preview_image_url);
assert.equal(feed.highlights[0].hasImage, true);
});
it("should add the image from the imageCache if it exists to the link", async () => {
links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
feed.imageCache = new Map([["https://mozilla.org", FAKE_IMAGE]]);
await feed.fetchHighlights();
assert.equal(feed.highlights[0].image, FAKE_IMAGE);
});
it("should call fetchImage with the correct arguments for each link", async () => {
links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
sinon.spy(feed, "fetchImage");
await feed.fetchHighlights();
assert.calledOnce(feed.fetchImage);
assert.calledWith(feed.fetchImage, links[0].url, links[0].preview_image_url);
});
it("should not include any links already in Top Sites", async () => {
links = [
@ -115,6 +133,38 @@ describe("Top Sites Feed", () => {
await feed.fetchHighlights();
assert.equal(feed.highlights[0].type, "bookmark");
});
it("should clear the imageCache at the end", async () => {
links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jpg"}];
feed.imageCache = new Map([["https://mozilla.org", FAKE_IMAGE]]);
// Stops fetchImage adding to the cache
feed.fetchImage = () => {};
await feed.fetchHighlights();
assert.equal(feed.imageCache.size, 0);
});
});
describe("#fetchImage", () => {
const FAKE_URL = "https://mozilla.org";
const FAKE_IMAGE_URL = "https://mozilla.org/preview.jpg";
it("should capture the image, if available", async () => {
await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
assert.calledOnce(fakeScreenshot.getScreenshotForURL);
assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_IMAGE_URL);
});
it("should fall back to capturing a screenshot", async () => {
await feed.fetchImage(FAKE_URL, undefined);
assert.calledOnce(fakeScreenshot.getScreenshotForURL);
assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_URL);
});
it("should store the image in the imageCache", async () => {
feed.imageCache.clear();
await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
assert.equal(feed.imageCache.get(FAKE_URL), FAKE_IMAGE);
});
it("should call SectionsManager.updateSectionCard with the right arguments", async () => {
await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
assert.calledOnce(sectionsManagerStub.updateSectionCard);
assert.calledWith(sectionsManagerStub.updateSectionCard, "highlights", FAKE_URL, {image: FAKE_IMAGE}, true);
});
});
describe("#uninit", () => {
it("should disable its section", () => {

View File

@ -57,7 +57,7 @@ describe("PrefsFeed", () => {
});
it("should set prerender pref to false if a pref does not match its initial value", () => {
Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
FAKE_PREFS.set("feeds.section.topstories", false);
FAKE_PREFS.set("showSearch", false);
feed.onAction({type: at.INIT});
assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, false);
});
@ -70,16 +70,16 @@ describe("PrefsFeed", () => {
it("should set the prerender pref to false if a pref in invalidatingPrefs is changed from its original value", () => {
Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
feed._prefs.set("feeds.section.topstories", false);
feed.onPrefChanged("feeds.section.topstories", false);
feed._prefs.set("showSearch", false);
feed.onPrefChanged("showSearch", false);
assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, false);
});
it("should set the prerender pref back to true if the invalidatingPrefs are changed back to their original values", () => {
Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name]));
FAKE_PREFS.set("feeds.section.topstories", false);
FAKE_PREFS.set("showSearch", false);
feed._prefs.set("feeds.section.topstories", true);
feed.onPrefChanged("feeds.section.topstories", true);
feed._prefs.set("showSearch", true);
feed.onPrefChanged("showSearch", true);
assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, true);
});
});

View File

@ -5,7 +5,9 @@ const {MAIN_MESSAGE_TYPE, CONTENT_MESSAGE_TYPE} = require("common/Actions.jsm");
const FAKE_ID = "FAKE_ID";
const FAKE_OPTIONS = {icon: "FAKE_ICON", title: "FAKE_TITLE"};
const FAKE_ROWS = [{url: "1"}, {url: "2"}, {"url": "3"}];
const FAKE_ROWS = [{url: "1.example.com"}, {url: "2.example.com"}, {"url": "3.example.com"}];
const FAKE_URL = "2.example.com";
const FAKE_CARD_OPTIONS = {title: "Some fake title"};
describe("SectionsManager", () => {
let globals;
@ -183,6 +185,24 @@ describe("SectionsManager", () => {
assert.calledWith(SectionsManager.once, SectionsManager.INIT, callback);
});
});
describe("#updateSectionCard", () => {
it("should emit an UPDATE_SECTION_CARD event with correct arguments", () => {
SectionsManager.addSection(FAKE_ID, Object.assign({}, FAKE_OPTIONS, {rows: FAKE_ROWS}));
const spy = sinon.spy();
SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy);
SectionsManager.updateSectionCard(FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS, true);
assert.calledOnce(spy);
assert.calledWith(spy, SectionsManager.UPDATE_SECTION_CARD, FAKE_ID,
FAKE_URL, FAKE_CARD_OPTIONS, true);
});
it("should do nothing if the section doesn't exist", () => {
SectionsManager.removeSection(FAKE_ID);
const spy = sinon.spy();
SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy);
SectionsManager.updateSectionCard(FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS, true);
assert.notCalled(spy);
});
});
});
describe("SectionsFeed", () => {
@ -204,11 +224,12 @@ describe("SectionsFeed", () => {
it("should bind appropriate listeners", () => {
sinon.spy(SectionsManager, "on");
feed.init();
assert.calledThrice(SectionsManager.on);
assert.callCount(SectionsManager.on, 4);
for (const [event, listener] of [
[SectionsManager.ADD_SECTION, feed.onAddSection],
[SectionsManager.REMOVE_SECTION, feed.onRemoveSection],
[SectionsManager.UPDATE_SECTION, feed.onUpdateSection]
[SectionsManager.UPDATE_SECTION, feed.onUpdateSection],
[SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard]
]) {
assert.calledWith(SectionsManager.on, event, listener);
}
@ -231,11 +252,12 @@ describe("SectionsFeed", () => {
sinon.spy(SectionsManager, "off");
feed.init();
feed.uninit();
assert.calledThrice(SectionsManager.off);
assert.callCount(SectionsManager.off, 4);
for (const [event, listener] of [
[SectionsManager.ADD_SECTION, feed.onAddSection],
[SectionsManager.REMOVE_SECTION, feed.onRemoveSection],
[SectionsManager.UPDATE_SECTION, feed.onUpdateSection]
[SectionsManager.UPDATE_SECTION, feed.onUpdateSection],
[SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard]
]) {
assert.calledWith(SectionsManager.off, event, listener);
}
@ -271,7 +293,7 @@ describe("SectionsFeed", () => {
});
});
describe("#onUpdateSection", () => {
it("should do nothing if no rows are provided", () => {
it("should do nothing if no options are provided", () => {
feed.onUpdateSection(null, FAKE_ID, null);
assert.notCalled(feed.store.dispatch);
});
@ -291,6 +313,27 @@ describe("SectionsFeed", () => {
assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
});
});
describe("#onUpdateSectionCard", () => {
it("should do nothing if no options are provided", () => {
feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, null);
assert.notCalled(feed.store.dispatch);
});
it("should dispatch a SECTION_UPDATE_CARD action with the correct data", () => {
feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS);
const action = feed.store.dispatch.firstCall.args[0];
assert.equal(action.type, "SECTION_UPDATE_CARD");
assert.deepEqual(action.data, {id: FAKE_ID, url: FAKE_URL, options: FAKE_CARD_OPTIONS});
// Should be not broadcast by default, so meta should not exist
assert.notOk(action.meta);
});
it("should broadcast the action only if shouldBroadcast is true", () => {
feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS, true);
const action = feed.store.dispatch.firstCall.args[0];
// Should be broadcast
assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);
assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);
});
});
describe("#onAction", () => {
it("should bind this.init to SectionsManager.INIT on INIT", () => {
sinon.spy(SectionsManager, "once");

View File

@ -99,10 +99,15 @@ describe("TelemetryFeed", () => {
assert.calledOnce(global.gUUIDGenerator.generateUUID);
assert.equal(session.session_id, global.gUUIDGenerator.generateUUID.firstCall.returnValue);
});
it("should set the page", () => {
it("should set the page if a url parameter is given", () => {
const session = instance.addSession("foo", "about:monkeys");
assert.propertyVal(session, "page", "about:monkeys");
});
it("should set the page prop to 'unknown' if no URL parameter given", () => {
const session = instance.addSession("foo");
assert.equal(session.page, "about:newtab"); // This is hardcoded for now.
assert.propertyVal(session, "page", "unknown");
});
it("should set the perf type when lacking timestamp", () => {
const session = instance.addSession("foo");
@ -178,11 +183,12 @@ describe("TelemetryFeed", () => {
const ping = await instance.createPing();
assert.validate(ping, BasePing);
assert.notProperty(ping, "session_id");
assert.notProperty(ping, "page");
});
it("should create a valid base ping with session info if a portID is supplied", async () => {
// Add a session
const portID = "foo";
instance.addSession(portID);
instance.addSession(portID, "about:home");
const sessionID = instance.sessions.get(portID).session_id;
// Create a ping referencing the session
@ -191,13 +197,13 @@ describe("TelemetryFeed", () => {
// Make sure we added the right session-related stuff to the ping
assert.propertyVal(ping, "session_id", sessionID);
assert.propertyVal(ping, "page", "about:newtab");
assert.propertyVal(ping, "page", "about:home");
});
it("should create an unexpected base ping if no session yet portID is supplied", async () => {
const ping = await instance.createPing("foo");
assert.validate(ping, BasePing);
assert.propertyVal(ping, "page", "about:newtab");
assert.propertyVal(ping, "page", "unknown");
assert.propertyVal(instance.sessions.get("foo").perf, "load_trigger_type", "unexpected");
});
it("should create a base ping with user_prefs", async () => {
@ -490,11 +496,11 @@ describe("TelemetryFeed", () => {
instance.onAction(ac.SendToMain({
type: at.NEW_TAB_INIT,
data: {}
data: {url: "about:monkeys"}
}, "port123"));
assert.calledOnce(stub);
assert.calledWith(stub, "port123");
assert.calledWith(stub, "port123", "about:monkeys");
});
it("should call .endSession() on a NEW_TAB_UNLOAD action", () => {
const stub = sandbox.stub(instance, "endSession");

View File

@ -385,6 +385,17 @@ describe("Top Sites Feed", () => {
data: [Object.assign({}, site1, {hostname: "foo.com"})]
}));
});
it("should compare against links if available, instead of getting from store", () => {
const frecentSite = {url: "foo.com", faviconSize: 32, favicon: "favicon.png"};
const pinnedSite1 = {url: "bar.com"};
const pinnedSite2 = {url: "foo.com"};
fakeNewTabUtils.pinnedLinks.links = [pinnedSite1, pinnedSite2];
feed.store = {getState() { return {TopSites: {rows: sinon.spy()}}; }};
let result = feed._getPinnedWithData([frecentSite]);
assert.deepEqual(result[0], pinnedSite1);
assert.deepEqual(result[1], Object.assign({}, frecentSite, pinnedSite2));
assert.notCalled(feed.store.getState().TopSites.rows);
});
it("should call unpin with correct parameters on TOP_SITES_UNPIN", () => {
fakeNewTabUtils.pinnedLinks.links = [null, null, {url: "foo.com"}, null, null, null, null, null, FAKE_LINKS[0]];
const unpinAction = {

Some files were not shown because too many files have changed in this diff Show More