merge autoland to mozilla-central. r=merge a=merge
MozReview-Commit-ID: 9SALJlvWgoZ
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -39,7 +39,6 @@ var CustomizationHandler = {
|
||||
UpdateUrlbarSearchSplitterState();
|
||||
|
||||
PlacesToolbarHelper.customizeStart();
|
||||
DownloadsButton.customizeStart();
|
||||
},
|
||||
|
||||
_customizationEnding(aDetails) {
|
||||
@ -63,7 +62,6 @@ var CustomizationHandler = {
|
||||
}
|
||||
|
||||
PlacesToolbarHelper.customizeDone();
|
||||
DownloadsButton.customizeDone();
|
||||
|
||||
UpdateUrlbarSearchSplitterState();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
@ -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`);
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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"},
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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"/>
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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");
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
},
|
||||
|
@ -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]
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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 },
|
||||
]);
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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});
|
||||
});
|
||||
}
|
||||
|
@ -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`);
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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", {
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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")) {
|
||||
|
@ -581,6 +581,7 @@
|
||||
&backgroundColor2.label;
|
||||
&useSystemColors.label;
|
||||
&underlineLinks.label;
|
||||
&links;
|
||||
&linkColor2.label;
|
||||
&visitedLinkColor2.label;"/>
|
||||
</hbox>
|
||||
|
@ -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();
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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"];
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -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";
|
||||
|
@ -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 {
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 4.1 KiB |
@ -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",
|
||||
|
@ -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ı Firefox’a taşıyabilirsiniz.",
|
||||
"manual_migration_explanation2": "Öteki tarayıcılarınızdaki yer imlerinizi, geçmişinizi ve parolalarınızı Firefox’a aktarabilirsiniz.",
|
||||
"manual_migration_cancel_button": "Gerek yok",
|
||||
"manual_migration_import_button": "Olur, aktaralım"
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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()
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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};
|
||||
|
@ -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");
|
||||
|
@ -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", () => {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
|
@ -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 = {
|
||||
|