diff --git a/.eslintignore b/.eslintignore
index 951a50edeee8..d48a8e380996 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -72,7 +72,8 @@ browser/extensions/pdfjs/content/web**
browser/extensions/pocket/content/panels/js/tmpl.js
browser/extensions/pocket/content/panels/js/vendor/**
browser/locales/**
-# vendor library files in activity-stream
+# generated or library files in activity-stream
+browser/extensions/activity-stream/data/content/activity-stream.bundle.js
browser/extensions/activity-stream/vendor/**
# imported from chromium
browser/extensions/mortar/**
diff --git a/accessible/.eslintrc.js b/accessible/.eslintrc.js
index 3cd32ed6019e..563570d8b953 100644
--- a/accessible/.eslintrc.js
+++ b/accessible/.eslintrc.js
@@ -13,5 +13,12 @@ module.exports = {
"dump": true,
"Services": true,
"XPCOMUtils": true
+ },
+ "rules": {
+ // Warn about cyclomatic complexity in functions.
+ "complexity": ["error", 42],
+
+ // Maximum depth callbacks can be nested.
+ "max-nested-callbacks": ["error", 10],
}
};
diff --git a/accessible/base/NotificationController.cpp b/accessible/base/NotificationController.cpp
index 8f296a0550df..4fe2072e7970 100644
--- a/accessible/base/NotificationController.cpp
+++ b/accessible/base/NotificationController.cpp
@@ -676,7 +676,7 @@ NotificationController::WillRefresh(mozilla::TimeStamp aTime)
}
#endif
- mDocument->ContentRemoved(containerElm, textNode);
+ mDocument->ContentRemoved(textAcc);
continue;
}
diff --git a/accessible/base/TreeWalker.cpp b/accessible/base/TreeWalker.cpp
index 8c04b5d6f740..9065826cdee5 100644
--- a/accessible/base/TreeWalker.cpp
+++ b/accessible/base/TreeWalker.cpp
@@ -52,6 +52,18 @@ TreeWalker::
MOZ_COUNT_CTOR(TreeWalker);
}
+TreeWalker::
+ TreeWalker(DocAccessible* aDocument, nsIContent* aAnchorNode) :
+ mDoc(aDocument), mContext(nullptr), mAnchorNode(aAnchorNode),
+ mARIAOwnsIdx(0),
+ mChildFilter(nsIContent::eSkipPlaceholderContent | nsIContent::eAllChildren),
+ mFlags(eWalkCache),
+ mPhase(eAtStart)
+{
+ MOZ_ASSERT(aAnchorNode, "No anchor node for the accessible tree walker");
+ MOZ_COUNT_CTOR(TreeWalker);
+}
+
TreeWalker::~TreeWalker()
{
MOZ_COUNT_DTOR(TreeWalker);
diff --git a/accessible/base/TreeWalker.h b/accessible/base/TreeWalker.h
index 377a5e3b8bb3..a8fe8230ae75 100644
--- a/accessible/base/TreeWalker.h
+++ b/accessible/base/TreeWalker.h
@@ -47,6 +47,11 @@ public:
*/
TreeWalker(Accessible* aContext, nsIContent* aAnchorNode, uint32_t aFlags = eWalkCache);
+ /**
+ * Navigates the accessible children within the anchor node subtree.
+ */
+ TreeWalker(DocAccessible* aDocument, nsIContent* aAnchorNode);
+
~TreeWalker();
/**
diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp
index fa7d89002097..161b2e9d8a10 100644
--- a/accessible/base/nsAccessibilityService.cpp
+++ b/accessible/base/nsAccessibilityService.cpp
@@ -524,7 +524,7 @@ nsAccessibilityService::DeckPanelSwitched(nsIPresShell* aPresShell,
}
#endif
- document->ContentRemoved(aDeckNode, panelNode);
+ document->ContentRemoved(panelNode);
}
if (aCurrentBoxFrame) {
@@ -582,26 +582,7 @@ nsAccessibilityService::ContentRemoved(nsIPresShell* aPresShell,
#endif
if (document) {
- // Flatten hierarchy may be broken at this point so we cannot get a true
- // container by traversing up the DOM tree. Find a parent of first accessible
- // from the subtree of the given DOM node, that'll be a container. If no
- // accessibles in subtree then we don't care about the change.
- Accessible* child = document->GetAccessible(aChildNode);
- if (!child) {
- Accessible* container = document->GetContainerAccessible(aChildNode);
- a11y::TreeWalker walker(container ? container : document, aChildNode,
- a11y::TreeWalker::eWalkCache);
- child = walker.Next();
- }
-
- if (child) {
- MOZ_DIAGNOSTIC_ASSERT(child->Parent(), "Unattached accessible from tree");
- document->ContentRemoved(child->Parent(), aChildNode);
-#ifdef A11Y_LOG
- if (logging::IsEnabled(logging::eTree))
- logging::AccessibleNNode("real container", child->Parent());
-#endif
- }
+ document->ContentRemoved(aChildNode);
}
#ifdef A11Y_LOG
diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp
index ea1f6045cae9..159b375e5454 100644
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -1170,10 +1170,7 @@ DocAccessible::ContentRemoved(nsIDocument* aDocument,
// This one and content removal notification from layout may result in
// double processing of same subtrees. If it pops up in profiling, then
// consider reusing a document node cache to reject these notifications early.
- Accessible* container = GetAccessibleOrContainer(aContainerNode);
- if (container) {
- UpdateTreeOnRemoval(container, aChildNode);
- }
+ ContentRemoved(aChildNode);
}
void
@@ -1382,7 +1379,7 @@ DocAccessible::RecreateAccessible(nsIContent* aContent)
// should be coalesced with normal show/hide events.
nsIContent* parent = aContent->GetFlattenedTreeParent();
- ContentRemoved(parent, aContent);
+ ContentRemoved(aContent);
ContentInserted(parent, aContent, aContent->GetNextSibling());
}
@@ -1972,35 +1969,38 @@ DocAccessible::FireEventsOnInsertion(Accessible* aContainer)
}
void
-DocAccessible::UpdateTreeOnRemoval(Accessible* aContainer, nsIContent* aChildNode)
+DocAccessible::ContentRemoved(Accessible* aContent)
{
- // If child node is not accessible then look for its accessible children.
- Accessible* child = GetAccessible(aChildNode);
+ MOZ_DIAGNOSTIC_ASSERT(aContent->Parent(), "Unattached accessible from tree");
+
#ifdef A11Y_LOG
logging::TreeInfo("process content removal", 0,
- "container", aContainer, "child", aChildNode);
+ "container", aContent->Parent(), "child", aContent, nullptr);
#endif
- TreeMutation mt(aContainer);
- if (child) {
- mt.BeforeRemoval(child);
- MOZ_ASSERT(aContainer == child->Parent(), "Wrong parent");
- aContainer->RemoveChild(child);
- UncacheChildrenInSubtree(child);
- mt.Done();
- return;
- }
-
- TreeWalker walker(aContainer, aChildNode, TreeWalker::eWalkCache);
- while (Accessible* child = walker.Next()) {
- mt.BeforeRemoval(child);
- MOZ_ASSERT(aContainer == child->Parent(), "Wrong parent");
- aContainer->RemoveChild(child);
- UncacheChildrenInSubtree(child);
- }
+ TreeMutation mt(aContent->Parent());
+ mt.BeforeRemoval(aContent);
+ aContent->Parent()->RemoveChild(aContent);
+ UncacheChildrenInSubtree(aContent);
mt.Done();
}
+void
+DocAccessible::ContentRemoved(nsIContent* aContentNode)
+{
+ // If child node is not accessible then look for its accessible children.
+ Accessible* acc = GetAccessible(aContentNode);
+ if (acc) {
+ ContentRemoved(acc);
+ }
+ else {
+ TreeWalker walker(this, aContentNode);
+ while (Accessible* acc = walker.Next()) {
+ ContentRemoved(acc);
+ }
+ }
+}
+
bool
DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement)
{
@@ -2051,7 +2051,7 @@ DocAccessible::ValidateARIAOwned()
// If DOM node doesn't have a frame anymore then shutdown its accessible.
if (child->Parent() && !child->GetFrame()) {
- UpdateTreeOnRemoval(child->Parent(), child->GetContent());
+ ContentRemoved(child);
children->RemoveElementAt(idx);
idx--;
continue;
diff --git a/accessible/generic/DocAccessible.h b/accessible/generic/DocAccessible.h
index afa01044f044..92eeed1bc798 100644
--- a/accessible/generic/DocAccessible.h
+++ b/accessible/generic/DocAccessible.h
@@ -342,18 +342,10 @@ public:
nsIContent* aEndChildNode);
/**
- * Notify the document accessible that content was removed.
+ * Update the tree on content removal.
*/
- void ContentRemoved(Accessible* aContainer, nsIContent* aChildNode)
- {
- // Update the whole tree of this document accessible when the container is
- // null (document element is removed).
- UpdateTreeOnRemoval((aContainer ? aContainer : this), aChildNode);
- }
- void ContentRemoved(nsIContent* aContainerNode, nsIContent* aChildNode)
- {
- ContentRemoved(AccessibleOrTrueContainer(aContainerNode), aChildNode);
- }
+ void ContentRemoved(Accessible* aContent);
+ void ContentRemoved(nsIContent* aContentNode);
/**
* Updates accessible tree when rendered text is changed.
@@ -512,11 +504,6 @@ protected:
*/
void ProcessInvalidationList();
- /**
- * Update the accessible tree for content removal.
- */
- void UpdateTreeOnRemoval(Accessible* aContainer, nsIContent* aChildNode);
-
/**
* Validates all aria-owns connections and updates the tree accordingly.
*/
diff --git a/accessible/tests/browser/.eslintrc.js b/accessible/tests/browser/.eslintrc.js
index 4c909e4ab391..f38b1b517867 100644
--- a/accessible/tests/browser/.eslintrc.js
+++ b/accessible/tests/browser/.eslintrc.js
@@ -62,7 +62,7 @@ module.exports = { // eslint-disable-line no-undef
"comma-dangle": ["error", "never"],
"comma-spacing": "error",
"comma-style": ["error", "last"],
- "complexity": ["error", 35],
+ "complexity": ["error", 20],
"consistent-this": "off",
"curly": ["error", "multi-line"],
"default-case": "off",
@@ -70,6 +70,7 @@ module.exports = { // eslint-disable-line no-undef
"dot-notation": "error",
"eol-last": "error",
"eqeqeq": "off",
+ "func-call-spacing": "error",
"func-names": "off",
"func-style": "off",
"generator-star": "off",
@@ -141,7 +142,6 @@ module.exports = { // eslint-disable-line no-undef
"no-shadow": "error",
"no-shadow-restricted-names": "error",
"no-space-before-semi": "off",
- "no-spaced-func": "error",
"no-sparse-arrays": "error",
"no-sync": "off",
"no-ternary": "off",
diff --git a/browser/.eslintrc.js b/browser/.eslintrc.js
index 679373e3b43a..4df38ac4f9c5 100644
--- a/browser/.eslintrc.js
+++ b/browser/.eslintrc.js
@@ -17,9 +17,6 @@ module.exports = {
// which is a valid use case.
"no-empty": "error",
- // No spaces between function name and parentheses
- "no-spaced-func": "error",
-
// Maximum depth callbacks can be nested.
"max-nested-callbacks": ["error", 8],
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 68271e4ca190..560cfd116116 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1530,7 +1530,6 @@ pref("dom.ipc.reportProcessHangs", false);
pref("dom.ipc.reportProcessHangs", true);
#endif
-pref("browser.reader.detectedFirstArticle", false);
// Don't limit how many nodes we care about on desktop:
pref("reader.parse-node-limit", 0);
diff --git a/browser/base/content/aboutDialog.xul b/browser/base/content/aboutDialog.xul
index 661f2c6419ac..c066710641db 100644
--- a/browser/base/content/aboutDialog.xul
+++ b/browser/base/content/aboutDialog.xul
@@ -71,7 +71,7 @@
diff --git a/browser/base/content/browser-fullScreenAndPointerLock.js b/browser/base/content/browser-fullScreenAndPointerLock.js
index 78bfcb1273a3..5dbaf80d8760 100644
--- a/browser/base/content/browser-fullScreenAndPointerLock.js
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
@@ -75,7 +75,7 @@ var PointerlockFsWarning = {
if (aOrigin) {
this._origin = aOrigin;
}
- let uri = BrowserUtils.makeURI(this._origin);
+ let uri = Services.io.newURI(this._origin);
let host = null;
try {
host = uri.host;
diff --git a/browser/base/content/browser-plugins.js b/browser/base/content/browser-plugins.js
index 4f6be28bc2a9..93cd8024f3e0 100644
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -222,7 +222,7 @@ var gPluginHandler = {
// page. That means that we also need to compare the actual locations to
// ensure we aren't getting a message from a Data URI that we're no longer
// looking at.
- let receivedURI = BrowserUtils.makeURI(location);
+ let receivedURI = Services.io.newURI(location);
if (!browser.documentURI.equalsExceptRef(receivedURI)) {
return;
}
@@ -318,7 +318,7 @@ var gPluginHandler = {
// page. That means that we also need to compare the actual locations to
// ensure we aren't getting a message from a Data URI that we're no longer
// looking at.
- let receivedURI = BrowserUtils.makeURI(location);
+ let receivedURI = Services.io.newURI(location);
if (!browser.documentURI.equalsExceptRef(receivedURI)) {
return;
}
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index c963fd73e286..f2eca5728a06 100755
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -468,7 +468,7 @@ const gStoragePressureObserver = {
Services.prefs.getIntPref("browser.storageManager.pressureNotification.usageThresholdGB");
let msg = "";
let buttons = [];
- let usage = parseInt(data);
+ let usage = subject.QueryInterface(Ci.nsISupportsPRUint64).data
let prefStrBundle = document.getElementById("bundle_preferences");
let brandShortName = document.getElementById("bundle_brand").getString("brandShortName");
let notificationBox = document.getElementById("high-priority-global-notificationbox");
@@ -4088,7 +4088,7 @@ function FillHistoryMenu(aParent) {
let item = existingIndex < children.length ?
children[existingIndex] : document.createElement("menuitem");
- let entryURI = BrowserUtils.makeURI(entry.url, entry.charset, null);
+ let entryURI = Services.io.newURI(entry.url, entry.charset);
item.setAttribute("uri", uri);
item.setAttribute("label", entry.title || uri);
item.setAttribute("index", j);
diff --git a/browser/base/content/content.js b/browser/base/content/content.js
index 8fd09bd402ba..8390be5e52ae 100644
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -576,7 +576,7 @@ var ClickEventHandler = {
if (docShell.mixedContentChannel) {
const sm = Services.scriptSecurityManager;
try {
- let targetURI = BrowserUtils.makeURI(href);
+ let targetURI = Services.io.newURI(href);
sm.checkSameOriginURI(docshell.mixedContentChannel.URI, targetURI, false);
json.allowMixedContent = true;
} catch (e) {}
@@ -692,7 +692,7 @@ var ClickEventHandler = {
// In case of XLink, we don't return the node we got href from since
// callers expect -like elements.
// Note: makeURI() will throw if aUri is not a valid URI.
- return [href ? BrowserUtils.makeURI(href, null, baseURI).spec : null, null,
+ return [href ? Services.io.newURI(href, null, baseURI).spec : null, null,
node && node.ownerDocument.nodePrincipal];
}
};
@@ -867,12 +867,10 @@ addMessageListener("ContextMenu:SearchFieldBookmarkData", (message) => {
let charset = node.ownerDocument.characterSet;
- let formBaseURI = BrowserUtils.makeURI(node.form.baseURI,
- charset);
+ let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
- let formURI = BrowserUtils.makeURI(node.form.getAttribute("action"),
- charset,
- formBaseURI);
+ let formURI = Services.io.newURI(node.form.getAttribute("action"), charset,
+ formBaseURI);
let spec = formURI.spec;
@@ -1462,8 +1460,8 @@ let OfflineApps = {
return null;
try {
- var contentURI = BrowserUtils.makeURI(aWindow.location.href, null, null);
- return BrowserUtils.makeURI(attr, aWindow.document.characterSet, contentURI);
+ return Services.io.newURI(attr, aWindow.document.characterSet,
+ Services.io.newURI(aWindow.location.href));
} catch (e) {
return null;
}
diff --git a/browser/base/content/tab-content.js b/browser/base/content/tab-content.js
index 6b4390a04cf9..6507c7e6da0d 100644
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -1003,7 +1003,7 @@ var RefreshBlocker = {
.getInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIRefreshURI);
- let URI = BrowserUtils.makeURI(data.URI, data.originCharset, null);
+ let URI = Services.io.newURI(data.URI, data.originCharset);
refreshURI.forceRefreshURI(URI, data.delay, true);
}
diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml
index 6b2df38d16b1..f124be6b6e06 100644
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2108,10 +2108,20 @@
break;
default:
getter = () => {
+ if (AppConstants.NIGHTLY_BUILD) {
+ let message =
+ `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`
+ console.log(message + new Error().stack);
+ }
this._insertBrowser(aTab);
return browser[name];
};
setter = (value) => {
+ if (AppConstants.NIGHTLY_BUILD) {
+ let message =
+ `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`
+ console.log(message + new Error().stack);
+ }
this._insertBrowser(aTab);
return browser[name] = value;
};
diff --git a/browser/base/content/test/general/browser_storagePressure_notification.js b/browser/base/content/test/general/browser_storagePressure_notification.js
index 8808f551b91f..ef776d3f4c87 100644
--- a/browser/base/content/test/general/browser_storagePressure_notification.js
+++ b/browser/base/content/test/general/browser_storagePressure_notification.js
@@ -4,7 +4,10 @@
function notifyStoragePressure(usage = 100) {
let notifyPromise = TestUtils.topicObserved("QuotaManager::StoragePressure", () => true);
- Services.obs.notifyObservers(null, "QuotaManager::StoragePressure", usage);
+ let usageWrapper = Cc["@mozilla.org/supports-PRUint64;1"]
+ .createInstance(Ci.nsISupportsPRUint64);
+ usageWrapper.data = usage;
+ Services.obs.notifyObservers(usageWrapper, "QuotaManager::StoragePressure");
return notifyPromise;
}
diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js
index 6162c8f34813..ad68ec440a8f 100644
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -36,10 +36,6 @@ var whitelist = new Set([
// security/manager/pki/resources/content/device_manager.js
{file: "chrome://pippki/content/load_device.xul"},
- // browser/modules/ReaderParent.jsm
- {file: "chrome://browser/skin/reader-tour.png"},
- {file: "chrome://browser/skin/reader-tour@2x.png"},
-
// Used by setting this url as a pref in about:config
{file: "chrome://browser/content/newtab/alternativeDefaultSites.json"},
@@ -222,11 +218,6 @@ var whitelist = new Set([
platforms: ["linux"]},
// Bug 1348559
{file: "chrome://pippki/content/resetpassword.xul"},
-
- // Bug 1344257
- {file: "resource://gre-resources/checkmark.svg"},
- {file: "resource://gre-resources/indeterminate-checkmark.svg"},
- {file: "resource://gre-resources/radio.svg"},
// Bug 1351078
{file: "resource://gre/modules/Battery.jsm"},
// Bug 1351070
diff --git a/browser/base/content/test/static/browser_parsable_css.js b/browser/base/content/test/static/browser_parsable_css.js
index cd92ec5aeb0d..981c980f64cd 100644
--- a/browser/base/content/test/static/browser_parsable_css.js
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -65,7 +65,7 @@ let whitelist = [
if (!Services.prefs.getBoolPref("full-screen-api.unprefix.enabled")) {
whitelist.push({
- sourceName: /res\/(ua|html)\.css$/i,
+ sourceName: /(?:res|gre-resources)\/(ua|html)\.css$/i,
errorMessage: /Unknown pseudo-class .*\bfullscreen\b/i,
isFromDevTools: false
});
diff --git a/browser/components/.eslintrc.js b/browser/components/.eslintrc.js
index c7f34c63b8b3..1444a7a8ad74 100644
--- a/browser/components/.eslintrc.js
+++ b/browser/components/.eslintrc.js
@@ -4,6 +4,6 @@ module.exports = {
rules: {
// XXX Bug 1326071 - This should be reduced down - probably to 20 or to
// be removed & synced with the mozilla/recommended value.
- "complexity": ["error", {"max": 69}],
+ "complexity": ["error", 61],
}
};
diff --git a/browser/components/customizableui/content/panelUI.inc.xul b/browser/components/customizableui/content/panelUI.inc.xul
index aee05be7454b..c704fa6367b2 100644
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -475,7 +475,7 @@
diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js
index fb997ef7160d..67b869aee58f 100644
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -149,6 +149,9 @@ const PanelUI = {
* @param aEvent the event (if any) that triggers showing the menu.
*/
show(aEvent) {
+ if (gPhotonStructure) {
+ this._ensureShortcutsShown();
+ }
return new Promise(resolve => {
this.ensureReady().then(() => {
if (this.panel.state == "open" ||
@@ -870,6 +873,22 @@ const PanelUI = {
return iconAnchor || candidate;
},
+ _addedShortcuts: false,
+ _ensureShortcutsShown() {
+ if (this._addedShortcuts) {
+ return;
+ }
+ this._addedShortcuts = true;
+ for (let button of this.mainView.querySelectorAll("toolbarbutton[key]")) {
+ let keyId = button.getAttribute("key");
+ let key = document.getElementById(keyId);
+ if (!key) {
+ continue;
+ }
+ button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
+ }
+ },
+
_notify(status, topic) {
Services.obs.notifyObservers(window, "panelUI-notification-" + topic, status);
}
diff --git a/browser/components/migration/ESEDBReader.jsm b/browser/components/migration/ESEDBReader.jsm
index 1e853cef5e5b..fc8f72f7e6fe 100644
--- a/browser/components/migration/ESEDBReader.jsm
+++ b/browser/components/migration/ESEDBReader.jsm
@@ -235,7 +235,7 @@ function loadLibraries() {
gLibs.ese = ctypes.open("esent.dll");
gLibs.kernel = ctypes.open("kernel32.dll");
KERNEL.FileTimeToSystemTime = gLibs.kernel.declare("FileTimeToSystemTime",
- ctypes.default_abi, ctypes.int, KERNEL.FILETIME.ptr, KERNEL.SYSTEMTIME.ptr);
+ ctypes.winapi_abi, ctypes.int, KERNEL.FILETIME.ptr, KERNEL.SYSTEMTIME.ptr);
declareESEFunctions();
}
diff --git a/browser/components/migration/MSMigrationUtils.jsm b/browser/components/migration/MSMigrationUtils.jsm
index effaa5f470fb..1afe1fb6d679 100644
--- a/browser/components/migration/MSMigrationUtils.jsm
+++ b/browser/components/migration/MSMigrationUtils.jsm
@@ -79,7 +79,7 @@ function CtypesKernelHelpers() {
this._functions.FileTimeToSystemTime =
this._libs.kernel32.declare("FileTimeToSystemTime",
- ctypes.default_abi,
+ ctypes.winapi_abi,
wintypes.BOOL,
this._structs.FILETIME.ptr,
this._structs.SYSTEMTIME.ptr);
diff --git a/browser/components/migration/tests/unit/test_Edge_db_migration.js b/browser/components/migration/tests/unit/test_Edge_db_migration.js
index 638bf8dad3ec..cd4692766dd8 100644
--- a/browser/components/migration/tests/unit/test_Edge_db_migration.js
+++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js
@@ -171,7 +171,7 @@ let eseDBWritingHelpers = {
loadLibraries();
KERNEL.SystemTimeToFileTime = gLibs.kernel.declare("SystemTimeToFileTime",
- ctypes.default_abi, ctypes.bool, KERNEL.SYSTEMTIME.ptr, KERNEL.FILETIME.ptr);
+ ctypes.winapi_abi, ctypes.bool, KERNEL.SYSTEMTIME.ptr, KERNEL.FILETIME.ptr);
declareESEFunction("CreateDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR,
ESE.JET_PCWSTR, ESE.JET_DBID.ptr, ESE.JET_GRBIT);
diff --git a/browser/components/migration/tests/unit/test_IE_cookies.js b/browser/components/migration/tests/unit/test_IE_cookies.js
index 37a7462f2b14..a34b2e325464 100644
--- a/browser/components/migration/tests/unit/test_IE_cookies.js
+++ b/browser/components/migration/tests/unit/test_IE_cookies.js
@@ -22,8 +22,11 @@ add_task(function* () {
_In_ LPCTSTR lpszCookieData
);
*/
+ // NOTE: Even though MSDN documentation does not indicate a calling convention,
+ // InternetSetCookieW is declared in SDK headers as __stdcall but is exported
+ // from wininet.dll without name mangling, so it is effectively winapi_abi
let setIECookie = wininet.declare("InternetSetCookieW",
- ctypes.default_abi,
+ ctypes.winapi_abi,
BOOL,
LPCTSTR,
LPCTSTR,
@@ -37,8 +40,11 @@ add_task(function* () {
_Inout_ LPDWORD lpdwSize
);
*/
+ // NOTE: Even though MSDN documentation does not indicate a calling convention,
+ // InternetGetCookieW is declared in SDK headers as __stdcall but is exported
+ // from wininet.dll without name mangling, so it is effectively winapi_abi
let getIECookie = wininet.declare("InternetGetCookieW",
- ctypes.default_abi,
+ ctypes.winapi_abi,
BOOL,
LPCTSTR,
LPCTSTR,
diff --git a/browser/components/preferences/in-content-old/tests/browser_bug731866.js b/browser/components/preferences/in-content-old/tests/browser_bug731866.js
index c1031d4128e6..1ba207b1caf7 100644
--- a/browser/components/preferences/in-content-old/tests/browser_bug731866.js
+++ b/browser/components/preferences/in-content-old/tests/browser_bug731866.js
@@ -4,6 +4,9 @@
Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
Components.utils.import("resource://gre/modules/NetUtil.jsm");
+const storageManagerDisabled = !SpecialPowers.getBoolPref("browser.storageManager.enabled");
+const offlineGroupDisabled = !SpecialPowers.getBoolPref("browser.preferences.offlineGroup.enabled");
+
function test() {
waitForExplicitFinish();
open_preferences(runTest);
@@ -20,6 +23,22 @@ function checkElements(expectedPane) {
element.id === "drmGroup") {
continue;
}
+ // The siteDataGroup in the Storage Management project is currently only pref-on on Nightly for testing purpose.
+ // During the test and the transition period, we have to check the pref to see if the siteDataGroup
+ // should be hidden always. This would be a bit bothersome, same as the offlineGroup as below.
+ // However, this checking is necessary to make sure we don't leak the siteDataGroup into beta/release build
+ if (element.id == "siteDataGroup" && storageManagerDisabled) {
+ is_element_hidden(element, "Disabled siteDataGroup should be hidden");
+ continue;
+ }
+ // The siteDataGroup in the Storage Management project will replace the offlineGroup eventually,
+ // so during the transition period, we have to check the pref to see if the offlineGroup
+ // should be hidden always. See the bug 1354530 for the details.
+ if (element.id == "offlineGroup" && offlineGroupDisabled) {
+ is_element_hidden(element, "Disabled offlineGroup should be hidden");
+ continue;
+ }
+
let attributeValue = element.getAttribute("data-category");
let suffix = " (id=" + element.id + ")";
if (attributeValue == "pane" + expectedPane) {
diff --git a/browser/components/preferences/in-content-old/tests/browser_bug795764_cachedisabled.js b/browser/components/preferences/in-content-old/tests/browser_bug795764_cachedisabled.js
index 3e9762a5d018..f9557a592d6d 100644
--- a/browser/components/preferences/in-content-old/tests/browser_bug795764_cachedisabled.js
+++ b/browser/components/preferences/in-content-old/tests/browser_bug795764_cachedisabled.js
@@ -7,28 +7,21 @@ Components.utils.import("resource://gre/modules/NetUtil.jsm");
function test() {
waitForExplicitFinish();
- let prefs = [
- "browser.cache.offline.enable",
- "browser.cache.disk.enable",
- "browser.cache.memory.enable",
- ];
- for (let pref of prefs) {
- Services.prefs.setBoolPref(pref, false);
- }
-
// Adding one fake site so that the SiteDataManager would run.
// Otherwise, without any site then it would just return so we would end up in not testing SiteDataManager.
let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("https://www.foo.com");
Services.perms.addFromPrincipal(principal, "persistent-storage", Ci.nsIPermissionManager.ALLOW_ACTION);
-
registerCleanupFunction(function() {
- for (let pref of prefs) {
- Services.prefs.clearUserPref(pref);
- }
Services.perms.removeFromPrincipal(principal, "persistent-storage");
});
- open_preferences(runTest);
+ SpecialPowers.pushPrefEnv({set: [
+ ["browser.cache.offline.enable", false],
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ["browser.storageManager.enabled", true],
+ ["browser.preferences.offlineGroup.enabled", true]
+ ]}).then(() => open_preferences(runTest));
}
function runTest(win) {
@@ -36,7 +29,6 @@ function runTest(win) {
let tab = win.document;
let elements = tab.getElementById("mainPrefPane").children;
- let offlineGroupDisabled = !SpecialPowers.getBoolPref("browser.preferences.offlineGroup.enabled");
// Test if advanced pane is opened correctly
win.gotoPref("paneAdvanced");
@@ -44,13 +36,6 @@ function runTest(win) {
if (element.nodeName == "preferences") {
continue;
}
- // The siteDataGroup in the Storage Management project will replace the offlineGroup eventually,
- // so during the transition period, we have to check the pref to see if the offlineGroup
- // should be hidden always. See the bug 1354530 for the details.
- if (element.id == "offlineGroup" && offlineGroupDisabled) {
- is_element_hidden(element, "Disabled offlineGroup should be hidden");
- continue;
- }
let attributeValue = element.getAttribute("data-category");
if (attributeValue == "paneAdvanced") {
is_element_visible(element, "Advanced elements should be visible");
diff --git a/browser/components/preferences/in-content/preferences.js b/browser/components/preferences/in-content/preferences.js
index 8fb791dccda5..2ebbccb13043 100644
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -148,14 +148,15 @@ function gotoPref(aCategory) {
let categories = document.getElementById("categories");
const kDefaultCategoryInternalName = "paneGeneral";
let hash = document.location.hash;
+
+ let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
+ let breakIndex = category.indexOf("-");
// Subcategories allow for selecting smaller sections of the preferences
// until proper search support is enabled (bug 1353954).
- let breakIndex = hash.indexOf("-");
- let subcategory = breakIndex != -1 && hash.substring(breakIndex + 1);
+ let subcategory = breakIndex != -1 && category.substring(breakIndex + 1);
if (subcategory) {
- hash = hash.substring(0, breakIndex);
+ category = category.substring(0, breakIndex);
}
- let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
category = friendlyPrefCategoryNameToInternalName(category);
// Updating the hash (below) or changing the selected category
diff --git a/browser/components/preferences/in-content/privacy.xul b/browser/components/preferences/in-content/privacy.xul
index de442c41c42b..01265b8ba810 100644
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -611,6 +611,7 @@
value="&browserContainersLearnMore.label;"/>
diff --git a/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js b/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js
index 6e558ed3eed6..5d897669beeb 100644
--- a/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js
+++ b/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js
@@ -7,6 +7,7 @@ registerCleanupFunction(function() {
Services.prefs.clearUserPref("browser.preferences.instantApply");
});
+// Test opening to the differerent panes and subcategories in Preferences
add_task(function*() {
let prefs = yield openPreferencesViaOpenPreferencesAPI("panePrivacy");
is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected");
@@ -36,6 +37,28 @@ add_task(function*() {
yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
+// Test opening Preferences with subcategory on an existing Preferences tab. See bug 1358475.
+add_task(function*() {
+ let prefs = yield openPreferencesViaOpenPreferencesAPI("general-search", {leaveOpen: true});
+ is(prefs.selectedPane, "paneGeneral", "General pane is selected by default");
+ let doc = gBrowser.contentDocument;
+ is(doc.location.hash, "#general", "The subcategory should be removed from the URI");
+ ok(doc.querySelector("#startupGroup").hidden, "Startup should be hidden when only Search is requested");
+ ok(!doc.querySelector("#engineList").hidden, "The search engine list should be visible when Search is requested");
+ // The reasons that here just call the `openPreferences` API without the helping function are
+ // - already opened one about:preferences tab up there and
+ // - the goal is to test on the existing tab and
+ // - using `openPreferencesViaOpenPreferencesAPI` would introduce more handling of additional about:blank and unneccessary event
+ openPreferences("privacy-reports");
+ let selectedPane = gBrowser.contentWindow.history.state;
+ is(selectedPane, "panePrivacy", "Privacy pane should be selected");
+ is(doc.location.hash, "#privacy", "The subcategory should be removed from the URI");
+ ok(doc.querySelector("#locationBarGroup").hidden, "Location Bar prefs should be hidden when only Reports are requested");
+ ok(!doc.querySelector("#header-privacy").hidden, "The header should be visible when a subcategory is requested");
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+
function openPreferencesViaHash(aPane) {
let deferred = Promise.defer();
gBrowser.selectedTab = gBrowser.addTab("about:preferences" + (aPane ? "#" + aPane : ""));
diff --git a/browser/components/preferences/in-content/tests/browser_bug731866.js b/browser/components/preferences/in-content/tests/browser_bug731866.js
index c592a89226ba..ae049eeb0ff4 100644
--- a/browser/components/preferences/in-content/tests/browser_bug731866.js
+++ b/browser/components/preferences/in-content/tests/browser_bug731866.js
@@ -4,9 +4,11 @@
Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
Components.utils.import("resource://gre/modules/NetUtil.jsm");
+const storageManagerDisabled = !SpecialPowers.getBoolPref("browser.storageManager.enabled");
+const offlineGroupDisabled = !SpecialPowers.getBoolPref("browser.preferences.offlineGroup.enabled");
+
function test() {
waitForExplicitFinish();
- SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
open_preferences(runTest);
}
@@ -21,12 +23,18 @@ function checkElements(expectedPane) {
element.id === "drmGroup") {
continue;
}
-
+ // The siteDataGroup in the Storage Management project is currently only pref-on on Nightly for testing purpose.
+ // During the test and the transition period, we have to check the pref to see if the siteDataGroup
+ // should be hidden always. This would be a bit bothersome, same as the offlineGroup as below.
+ // However, this checking is necessary to make sure we don't leak the siteDataGroup into beta/release build
+ if (element.id == "siteDataGroup" && storageManagerDisabled) {
+ is_element_hidden(element, "Disabled siteDataGroup should be hidden");
+ continue;
+ }
// The siteDataGroup in the Storage Management project will replace the offlineGroup eventually,
// so during the transition period, we have to check the pref to see if the offlineGroup
// should be hidden always. See the bug 1354530 for the details.
- if (element.id == "offlineGroup" &&
- !SpecialPowers.getBoolPref("browser.preferences.offlineGroup.enabled")) {
+ if (element.id == "offlineGroup" && offlineGroupDisabled) {
is_element_hidden(element, "Disabled offlineGroup should be hidden");
continue;
}
diff --git a/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js b/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js
index c056834aec68..9759368e48d9 100644
--- a/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js
+++ b/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js
@@ -8,28 +8,21 @@ Cu.import("resource://gre/modules/NetUtil.jsm");
function test() {
waitForExplicitFinish();
- let prefs = [
- "browser.cache.offline.enable",
- "browser.cache.disk.enable",
- "browser.cache.memory.enable",
- ];
- for (let pref of prefs) {
- Services.prefs.setBoolPref(pref, false);
- }
-
// Adding one fake site so that the SiteDataManager would run.
// Otherwise, without any site then it would just return so we would end up in not testing SiteDataManager.
let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("https://www.foo.com");
Services.perms.addFromPrincipal(principal, "persistent-storage", Ci.nsIPermissionManager.ALLOW_ACTION);
-
registerCleanupFunction(function() {
- for (let pref of prefs) {
- Services.prefs.clearUserPref(pref);
- }
Services.perms.removeFromPrincipal(principal, "persistent-storage");
});
- open_preferences(runTest);
+ SpecialPowers.pushPrefEnv({set: [
+ ["browser.cache.offline.enable", false],
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ["browser.storageManager.enabled", true],
+ ["browser.preferences.offlineGroup.enabled", true]
+ ]}).then(() => open_preferences(runTest));
}
function runTest(win) {
@@ -37,7 +30,6 @@ function runTest(win) {
let tab = win.document;
let elements = tab.getElementById("mainPrefPane").children;
- let offlineGroupDisabled = !SpecialPowers.getBoolPref("browser.preferences.offlineGroup.enabled");
// Test if privacy pane is opened correctly
win.gotoPref("panePrivacy");
@@ -45,13 +37,6 @@ function runTest(win) {
if (element.nodeName == "preferences") {
continue;
}
- // The siteDataGroup in the Storage Management project will replace the offlineGroup eventually,
- // so during the transition period, we have to check the pref to see if the offlineGroup
- // should be hidden always. See the bug 1354530 for the details.
- if (element.id == "offlineGroup" && offlineGroupDisabled) {
- is_element_hidden(element, "Disabled offlineGroup should be hidden");
- continue;
- }
let attributeValue = element.getAttribute("data-category");
if (attributeValue == "panePrivacy") {
is_element_visible(element, "Privacy elements should be visible");
diff --git a/browser/components/sessionstore/SessionSaver.jsm b/browser/components/sessionstore/SessionSaver.jsm
index d89d36278e85..f612296c9154 100644
--- a/browser/components/sessionstore/SessionSaver.jsm
+++ b/browser/components/sessionstore/SessionSaver.jsm
@@ -242,7 +242,7 @@ var SessionSaverInternal = {
return Promise.resolve();
}
- stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
+ stopWatchStart("COLLECT_DATA_MS");
let state = SessionStore.getCurrentState(forceUpdateAllWindows);
PrivacyFilter.filterPrivateWindowsAndTabs(state);
@@ -274,26 +274,44 @@ var SessionSaverInternal = {
}
}
- // Clear all cookies and storage on clean shutdown according to user preferences
- if (RunState.isClosing) {
- let expireCookies = Services.prefs.getIntPref("network.cookie.lifetimePolicy") ==
- Services.cookies.QueryInterface(Ci.nsICookieService).ACCEPT_SESSION;
- let sanitizeCookies = Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") &&
- Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies");
- let restart = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
- // Don't clear when restarting
- if ((expireCookies || sanitizeCookies) && !restart) {
- for (let window of state.windows) {
- delete window.cookies;
- for (let tab of window.tabs) {
- delete tab.storage;
- }
+ // Clear cookies and storage on clean shutdown.
+ this._maybeClearCookiesAndStorage(state);
+
+ stopWatchFinish("COLLECT_DATA_MS");
+ return this._writeState(state);
+ },
+
+ /**
+ * Purges cookies and DOMSessionStorage data from the session on clean
+ * shutdown, only if requested by the user's preferences.
+ */
+ _maybeClearCookiesAndStorage(state) {
+ // Only do this on shutdown.
+ if (!RunState.isClosing) {
+ return;
+ }
+
+ // Don't clear when restarting.
+ if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) {
+ return;
+ }
+
+ let expireCookies = Services.prefs.getIntPref("network.cookie.lifetimePolicy") ==
+ Services.cookies.QueryInterface(Ci.nsICookieService).ACCEPT_SESSION;
+ let sanitizeCookies = Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") &&
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies");
+
+ if (expireCookies || sanitizeCookies) {
+ // Remove cookies.
+ delete state.cookies;
+
+ // Remove DOMSessionStorage data.
+ for (let window of state.windows) {
+ for (let tab of window.tabs) {
+ delete tab.storage;
}
}
}
-
- stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
- return this._writeState(state);
},
/**
diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm
index d75b11ddc457..803c3851b452 100644
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -3138,9 +3138,7 @@ var SessionStoreInternal = {
};
// Collect and store session cookies.
- TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
state.cookies = SessionCookies.collect();
- TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
if (Cu.isModuleLoaded("resource://devtools/client/scratchpad/scratchpad-manager.jsm")) {
// get open Scratchpad window states too
@@ -3336,7 +3334,7 @@ var SessionStoreInternal = {
}
}
- if (selectTab > 0) {
+ if (selectTab > 0 && selectTab <= tabs.length) {
// The state we're restoring wants to select a particular tab. This
// implies that we're overwriting tabs.
let currentIndex = tabbrowser.tabContainer.selectedIndex;
@@ -3851,7 +3849,7 @@ var SessionStoreInternal = {
browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent",
{loadArguments: aLoadArguments, isRemotenessUpdate,
- reason: aReason});
+ reason: aReason, requestTime: Services.telemetry.msSystemNow()});
},
/**
@@ -4406,8 +4404,8 @@ var SessionStoreInternal = {
if (tIndex + 1 < window.selected)
window.selected -= 1;
else if (tIndex + 1 == window.selected)
- pinnedWindowState.selected = pinnedWindowState.tabs.length + 2;
- // + 2 because the tab isn't actually in the array yet
+ pinnedWindowState.selected = pinnedWindowState.tabs.length + 1;
+ // + 1 because the tab isn't actually in the array yet
// Now add the pinned tab to our window
pinnedWindowState.tabs =
diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js
index 284a872d9dc6..02ef95b3db87 100644
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -144,6 +144,11 @@ var MessageListener = {
this.restoreHistory(data);
break;
case "SessionStore:restoreTabContent":
+ if (data.isRemotenessUpdate) {
+ let histogram = Services.telemetry.getKeyedHistogramById("FX_TAB_REMOTE_NAVIGATION_DELAY_MS");
+ histogram.add("SessionStore:restoreTabContent",
+ Services.telemetry.msSystemNow() - data.requestTime);
+ }
this.restoreTabContent(data);
break;
case "SessionStore:resetRestore":
diff --git a/browser/components/sessionstore/test/browser_async_window_flushing.js b/browser/components/sessionstore/test/browser_async_window_flushing.js
index 58885b1e3055..bfa55ac2581c 100644
--- a/browser/components/sessionstore/test/browser_async_window_flushing.js
+++ b/browser/components/sessionstore/test/browser_async_window_flushing.js
@@ -104,8 +104,7 @@ add_task(function* test_remove_uninteresting_window() {
// history entries and make itself seem uninteresting.
yield ContentTask.spawn(browser, null, function*() {
// Epic hackery to make this browser seem suddenly boring.
- Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
- docShell.setCurrentURI(BrowserUtils.makeURI("about:blank"));
+ docShell.setCurrentURI(Services.io.newURI("about:blank"));
let {sessionHistory} = docShell.QueryInterface(Ci.nsIWebNavigation);
sessionHistory.PurgeHistory(sessionHistory.count);
diff --git a/browser/config/mozconfigs/linux64/code-coverage b/browser/config/mozconfigs/linux64/code-coverage
index 595b73d6b8b0..81785aa3dbf0 100644
--- a/browser/config/mozconfigs/linux64/code-coverage
+++ b/browser/config/mozconfigs/linux64/code-coverage
@@ -6,6 +6,7 @@ ac_add_options --disable-install-strip
ac_add_options --disable-jemalloc
ac_add_options --disable-crashreporter
ac_add_options --disable-elf-hack
+ac_add_options --enable-debug
MOZ_CODE_COVERAGE=1
export CFLAGS="--coverage"
diff --git a/browser/extensions/activity-stream/bootstrap.js b/browser/extensions/activity-stream/bootstrap.js
index 2d26688bdf32..e22c79d5c817 100644
--- a/browser/extensions/activity-stream/bootstrap.js
+++ b/browser/extensions/activity-stream/bootstrap.js
@@ -1,16 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* globals Components, XPCOMUtils, Preferences, ActivityStream */
+/* globals Components, XPCOMUtils, Preferences, Services, ActivityStream */
"use strict";
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ActivityStream",
"resource://activity-stream/lib/ActivityStream.jsm");
+const BROWSER_READY_NOTIFICATION = "browser-ui-startup-complete";
const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled";
const REASON_STARTUP_ON_PREF_CHANGE = "PREF_ON";
const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
@@ -19,6 +21,7 @@ const ACTIVITY_STREAM_OPTIONS = {newTabURL: "about:newtab"};
let activityStream;
let startupData;
+let startupReason;
/**
* init - Initializes an instance of ActivityStream. This could be called by
@@ -64,27 +67,38 @@ function onPrefChanged(isEnabled) {
}
}
+function observe(subject, topic, data) {
+ switch (topic) {
+ case BROWSER_READY_NOTIFICATION:
+ // Listen for changes to the pref that enables Activity Stream
+ Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+ // Only initialize if the pref is true
+ if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
+ init(startupReason);
+ Services.obs.removeObserver(this, BROWSER_READY_NOTIFICATION);
+ }
+ break;
+ }
+}
+
// The functions below are required by bootstrap.js
this.install = function install(data, reason) {};
this.startup = function startup(data, reason) {
+ // Only start Activity Stream up when the browser UI is ready
+ Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION);
+
// Cache startup data which contains stuff like the version number, etc.
// so we can use it when we init
startupData = data;
-
- // Listen for changes to the pref that enables Activity Stream
- Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
-
- // Only initialize if the pref is true
- if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
- init(reason);
- }
+ startupReason = reason;
};
this.shutdown = function shutdown(data, reason) {
// Uninitialize Activity Stream
startupData = null;
+ startupReason = null;
uninit(reason);
// Stop listening to the pref that enables Activity Stream
diff --git a/browser/extensions/activity-stream/common/Actions.jsm b/browser/extensions/activity-stream/common/Actions.jsm
index bb1654a85b39..56ac20e88834 100644
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -3,15 +3,19 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-this.MAIN_MESSAGE_TYPE = "ActivityStream:Main";
-this.CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
+const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
+const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
-this.actionTypes = [
+const actionTypes = [
"INIT",
"UNINIT",
"NEW_TAB_INITIAL_STATE",
"NEW_TAB_LOAD",
- "NEW_TAB_UNLOAD"
+ "NEW_TAB_UNLOAD",
+ "PERFORM_SEARCH",
+ "SCREENSHOT_UPDATED",
+ "SEARCH_STATE_UPDATED",
+ "TOP_SITES_UPDATED"
// The line below creates an object like this:
// {
// INIT: "INIT",
@@ -86,6 +90,8 @@ function SendToContent(action, target) {
});
}
+this.actionTypes = actionTypes;
+
this.actionCreators = {
SendToMain,
SendToContent,
diff --git a/browser/extensions/activity-stream/common/Reducers.jsm b/browser/extensions/activity-stream/common/Reducers.jsm
index 33b6db8a44d4..048aa1112afe 100644
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -3,42 +3,63 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-this.INITIAL_STATE = {
+const {actionTypes: at} = Components.utils.import("resource://activity-stream/common/Actions.jsm", {});
+
+const INITIAL_STATE = {
TopSites: {
- rows: [
- {
- "title": "Facebook",
- "url": "https://www.facebook.com/"
- },
- {
- "title": "YouTube",
- "url": "https://www.youtube.com/"
- },
- {
- "title": "Amazon",
- "url": "http://www.amazon.com/"
- },
- {
- "title": "Yahoo",
- "url": "https://www.yahoo.com/"
- },
- {
- "title": "eBay",
- "url": "http://www.ebay.com"
- },
- {
- "title": "Twitter",
- "url": "https://twitter.com/"
- }
- ]
+ init: false,
+ rows: []
+ },
+ Search: {
+ currentEngine: {
+ name: "",
+ icon: ""
+ },
+ engines: []
}
};
// TODO: Handle some real actions here, once we have a TopSites feed working
function TopSites(prevState = INITIAL_STATE.TopSites, action) {
- return prevState;
+ let hasMatch;
+ let newRows;
+ switch (action.type) {
+ case at.TOP_SITES_UPDATED:
+ if (!action.data) {
+ return prevState;
+ }
+ return Object.assign({}, prevState, {init: true, rows: action.data});
+ case at.SCREENSHOT_UPDATED:
+ newRows = prevState.rows.map(row => {
+ if (row.url === action.data.url) {
+ hasMatch = true;
+ return Object.assign({}, row, {screenshot: action.data.screenshot});
+ }
+ return row;
+ });
+ return hasMatch ? Object.assign({}, prevState, {rows: newRows}) : prevState;
+ default:
+ return prevState;
+ }
}
-this.reducers = {TopSites};
+function Search(prevState = INITIAL_STATE.Search, action) {
+ switch (action.type) {
+ case at.SEARCH_STATE_UPDATED: {
+ if (!action.data) {
+ return prevState;
+ }
+ let {currentEngine, engines} = action.data;
+ return Object.assign({}, prevState, {
+ currentEngine,
+ engines
+ });
+ }
+ default:
+ return prevState;
+ }
+}
+this.INITIAL_STATE = INITIAL_STATE;
+this.reducers = {TopSites, Search};
this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"];
diff --git a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
new file mode 100644
index 000000000000..e9c00838f8be
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -0,0 +1,570 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 10);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports) {
+
+module.exports = React;
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports) {
+
+module.exports = ReactRedux;
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
+const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
+
+const actionTypes = ["INIT", "UNINIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "PERFORM_SEARCH", "SCREENSHOT_UPDATED", "SEARCH_STATE_UPDATED", "TOP_SITES_UPDATED"
+// The line below creates an object like this:
+// {
+// INIT: "INIT",
+// UNINIT: "UNINIT"
+// }
+// It prevents accidentally adding a different key/value name.
+].reduce((obj, type) => {
+ obj[type] = type;return obj;
+}, {});
+
+// Helper function for creating routed actions between content and main
+// Not intended to be used by consumers
+function _RouteMessage(action, options) {
+ const meta = action.meta ? Object.assign({}, action.meta) : {};
+ if (!options || !options.from || !options.to) {
+ throw new Error("Routed Messages must have options as the second parameter, and must at least include a .from and .to property.");
+ }
+ // For each of these fields, if they are passed as an option,
+ // add them to the action. If they are not defined, remove them.
+ ["from", "to", "toTarget", "fromTarget", "skipOrigin"].forEach(o => {
+ if (typeof options[o] !== "undefined") {
+ meta[o] = options[o];
+ } else if (meta[o]) {
+ delete meta[o];
+ }
+ });
+ return Object.assign({}, action, { meta });
+}
+
+/**
+ * SendToMain - Creates a message that will be sent to the Main process.
+ *
+ * @param {object} action Any redux action (required)
+ * @param {object} options
+ * @param {string} options.fromTarget The id of the content port from which the action originated. (optional)
+ * @return {object} An action with added .meta properties
+ */
+function SendToMain(action) {
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ return _RouteMessage(action, {
+ from: CONTENT_MESSAGE_TYPE,
+ to: MAIN_MESSAGE_TYPE,
+ fromTarget: options.fromTarget
+ });
+}
+
+/**
+ * BroadcastToContent - Creates a message that will be sent to ALL content processes.
+ *
+ * @param {object} action Any redux action (required)
+ * @return {object} An action with added .meta properties
+ */
+function BroadcastToContent(action) {
+ return _RouteMessage(action, {
+ from: MAIN_MESSAGE_TYPE,
+ to: CONTENT_MESSAGE_TYPE
+ });
+}
+
+/**
+ * SendToContent - Creates a message that will be sent to a particular Content process.
+ *
+ * @param {object} action Any redux action (required)
+ * @param {string} target The id of a content port
+ * @return {object} An action with added .meta properties
+ */
+function SendToContent(action, target) {
+ if (!target) {
+ throw new Error("You must provide a target ID as the second parameter of SendToContent. If you want to send to all content processes, use BroadcastToContent");
+ }
+ return _RouteMessage(action, {
+ from: MAIN_MESSAGE_TYPE,
+ to: CONTENT_MESSAGE_TYPE,
+ toTarget: target
+ });
+}
+
+var actionCreators = {
+ SendToMain,
+ SendToContent,
+ BroadcastToContent
+};
+
+// These are helpers to test for certain kinds of actions
+
+var actionUtils = {
+ isSendToMain(action) {
+ if (!action.meta) {
+ return false;
+ }
+ return action.meta.to === MAIN_MESSAGE_TYPE && action.meta.from === CONTENT_MESSAGE_TYPE;
+ },
+ isBroadcastToContent(action) {
+ if (!action.meta) {
+ return false;
+ }
+ if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) {
+ return true;
+ }
+ return false;
+ },
+ isSendToContent(action) {
+ if (!action.meta) {
+ return false;
+ }
+ if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) {
+ return true;
+ }
+ return false;
+ },
+ _RouteMessage
+};
+module.exports = {
+ actionTypes,
+ actionCreators,
+ actionUtils,
+ MAIN_MESSAGE_TYPE,
+ CONTENT_MESSAGE_TYPE
+};
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+const TopSites = __webpack_require__(8);
+const Search = __webpack_require__(7);
+
+const Base = () => React.createElement(
+ "div",
+ { className: "outer-wrapper" },
+ React.createElement(
+ "main",
+ null,
+ React.createElement(Search, null),
+ React.createElement(TopSites, null)
+ )
+);
+
+module.exports = Base;
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* globals sendAsyncMessage, addMessageListener */
+
+var _require = __webpack_require__(9);
+
+const createStore = _require.createStore,
+ combineReducers = _require.combineReducers,
+ applyMiddleware = _require.applyMiddleware;
+
+var _require2 = __webpack_require__(2);
+
+const au = _require2.actionUtils;
+
+
+const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
+const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain";
+const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent";
+
+/**
+ * A higher-order function which returns a reducer that, on MERGE_STORE action,
+ * will return the action.data object merged into the previous state.
+ *
+ * For all other actions, it merely calls mainReducer.
+ *
+ * Because we want this to merge the entire state object, it's written as a
+ * higher order function which takes the main reducer (itself often a call to
+ * combineReducers) as a parameter.
+ *
+ * @param {function} mainReducer reducer to call if action != MERGE_STORE_ACTION
+ * @return {function} a reducer that, on MERGE_STORE_ACTION action,
+ * will return the action.data object merged
+ * into the previous state, and the result
+ * of calling mainReducer otherwise.
+ */
+function mergeStateReducer(mainReducer) {
+ return (prevState, action) => {
+ if (action.type === MERGE_STORE_ACTION) {
+ return Object.assign({}, prevState, action.data);
+ }
+
+ return mainReducer(prevState, action);
+ };
+}
+
+/**
+ * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary
+ */
+const messageMiddleware = store => next => action => {
+ if (au.isSendToMain(action)) {
+ sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
+ }
+ next(action);
+};
+
+/**
+ * initStore - Create a store and listen for incoming actions
+ *
+ * @param {object} reducers An object containing Redux reducers
+ * @return {object} A redux store
+ */
+module.exports = function initStore(reducers) {
+ const store = createStore(mergeStateReducer(combineReducers(reducers)), applyMiddleware(messageMiddleware));
+
+ addMessageListener(INCOMING_MESSAGE_NAME, msg => {
+ store.dispatch(msg.data);
+ });
+
+ return store;
+};
+
+module.exports.MERGE_STORE_ACTION = MERGE_STORE_ACTION;
+module.exports.OUTGOING_MESSAGE_NAME = OUTGOING_MESSAGE_NAME;
+module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+var _require = __webpack_require__(2);
+
+const at = _require.actionTypes;
+
+
+const INITIAL_STATE = {
+ TopSites: {
+ init: false,
+ rows: []
+ },
+ Search: {
+ currentEngine: {
+ name: "",
+ icon: ""
+ },
+ engines: []
+ }
+};
+
+// TODO: Handle some real actions here, once we have a TopSites feed working
+function TopSites() {
+ let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.TopSites;
+ let action = arguments[1];
+
+ let hasMatch;
+ let newRows;
+ switch (action.type) {
+ case at.TOP_SITES_UPDATED:
+ if (!action.data) {
+ return prevState;
+ }
+ return Object.assign({}, prevState, { init: true, rows: action.data });
+ case at.SCREENSHOT_UPDATED:
+ newRows = prevState.rows.map(row => {
+ if (row.url === action.data.url) {
+ hasMatch = true;
+ return Object.assign({}, row, { screenshot: action.data.screenshot });
+ }
+ return row;
+ });
+ return hasMatch ? Object.assign({}, prevState, { rows: newRows }) : prevState;
+ default:
+ return prevState;
+ }
+}
+
+function Search() {
+ let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Search;
+ let action = arguments[1];
+
+ switch (action.type) {
+ case at.SEARCH_STATE_UPDATED:
+ {
+ if (!action.data) {
+ return prevState;
+ }
+ var _action$data = action.data;
+ let currentEngine = _action$data.currentEngine,
+ engines = _action$data.engines;
+
+ return Object.assign({}, prevState, {
+ currentEngine,
+ engines
+ });
+ }
+ default:
+ return prevState;
+ }
+}
+var reducers = { TopSites, Search };
+module.exports = {
+ reducers,
+ INITIAL_STATE
+};
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports) {
+
+module.exports = ReactDOM;
+
+/***/ }),
+/* 7 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(1);
+
+const connect = _require.connect;
+
+var _require2 = __webpack_require__(2);
+
+const actionTypes = _require2.actionTypes,
+ actionCreators = _require2.actionCreators;
+
+
+const Search = React.createClass({
+ displayName: "Search",
+
+ getInitialState() {
+ return { searchString: "" };
+ },
+ performSearch(options) {
+ let searchData = {
+ engineName: options.engineName,
+ searchString: options.searchString,
+ searchPurpose: "newtab",
+ healthReportKey: "newtab"
+ };
+ this.props.dispatch(actionCreators.SendToMain({ type: actionTypes.PERFORM_SEARCH, data: searchData }));
+ },
+ onClick(event) {
+ const currentEngine = this.props.Search.currentEngine;
+
+ event.preventDefault();
+ this.performSearch({ engineName: currentEngine.name, searchString: this.state.searchString });
+ },
+ onChange(event) {
+ this.setState({ searchString: event.target.value });
+ },
+ render() {
+ return React.createElement(
+ "form",
+ { className: "search-wrapper" },
+ React.createElement("span", { className: "search-label" }),
+ React.createElement("input", { value: this.state.searchString, type: "search",
+ onChange: this.onChange,
+ maxLength: "256", title: "Submit search",
+ placeholder: "Search the Web" }),
+ React.createElement("button", { onClick: this.onClick })
+ );
+ }
+});
+
+module.exports = connect(state => ({ Search: state.Search }))(Search);
+
+/***/ }),
+/* 8 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(1);
+
+const connect = _require.connect;
+
+
+function displayURL(url) {
+ return new URL(url).hostname.replace(/^www./, "");
+}
+
+const TopSites = props => React.createElement(
+ "section",
+ null,
+ React.createElement(
+ "h3",
+ { className: "section-title" },
+ "Top Sites"
+ ),
+ React.createElement(
+ "ul",
+ { className: "top-sites-list" },
+ props.TopSites.rows.map(link => {
+ const title = displayURL(link.url);
+ const className = `screenshot${link.screenshot ? " active" : ""}`;
+ const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
+ return React.createElement(
+ "li",
+ { key: link.url },
+ React.createElement(
+ "a",
+ { href: link.url },
+ React.createElement(
+ "div",
+ { className: "tile" },
+ React.createElement(
+ "span",
+ { className: "letter-fallback", ariaHidden: true },
+ title[0]
+ ),
+ React.createElement("div", { className: className, style: style })
+ ),
+ React.createElement(
+ "div",
+ { className: "title" },
+ title
+ )
+ )
+ );
+ })
+ )
+);
+
+module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSites);
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports) {
+
+module.exports = Redux;
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* globals addMessageListener, removeMessageListener */
+const React = __webpack_require__(0);
+const ReactDOM = __webpack_require__(6);
+const Base = __webpack_require__(3);
+
+var _require = __webpack_require__(1);
+
+const Provider = _require.Provider;
+
+const initStore = __webpack_require__(4);
+
+var _require2 = __webpack_require__(5);
+
+const reducers = _require2.reducers;
+
+
+const store = initStore(reducers);
+
+ReactDOM.render(React.createElement(
+ Provider,
+ { store: store },
+ React.createElement(Base, null)
+), document.getElementById("root"));
+
+/***/ })
+/******/ ]);
\ No newline at end of file
diff --git a/browser/extensions/activity-stream/data/content/activity-stream.css b/browser/extensions/activity-stream/data/content/activity-stream.css
new file mode 100644
index 000000000000..c1cad86f6191
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -0,0 +1,334 @@
+html {
+ box-sizing: border-box; }
+
+*,
+*::before,
+*::after {
+ box-sizing: inherit; }
+
+body {
+ margin: 0; }
+
+button,
+input {
+ font-family: inherit;
+ font-size: inherit; }
+
+[hidden] {
+ display: none !important; }
+
+html,
+body,
+#root {
+ height: 100%; }
+
+body {
+ background: #F6F6F8;
+ color: #383E49;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif;
+ font-size: 16px; }
+
+h1,
+h2 {
+ font-weight: normal; }
+
+a {
+ color: #00AFF7;
+ text-decoration: none; }
+ a:hover {
+ color: #2bc1ff; }
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0; }
+
+.inner-border {
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 3px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 100; }
+
+@keyframes fadeIn {
+ from {
+ opacity: 0; }
+ to {
+ opacity: 1; } }
+
+.show-on-init {
+ opacity: 0;
+ transition: opacity 0.2s ease-in; }
+ .show-on-init.on {
+ opacity: 1;
+ animation: fadeIn 0.2s; }
+
+.actions {
+ border-top: solid 1px rgba(0, 0, 0, 0.1);
+ display: flex;
+ flex-direction: row;
+ margin: 0;
+ padding: 15px 25px;
+ justify-content: flex-start; }
+ .actions button {
+ background: #FBFBFB;
+ border: solid 1px #BFBFBF;
+ border-radius: 5px;
+ color: #858585;
+ cursor: pointer;
+ padding: 10px 30px; }
+ .actions button:hover {
+ box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
+ transition: box-shadow 150ms; }
+ .actions button.done {
+ background: #0695F9;
+ border: solid 1px #1677CF;
+ color: #FFF;
+ margin-inline-start: auto; }
+
+.outer-wrapper {
+ display: flex;
+ flex-grow: 1;
+ padding: 62px 32px 32px;
+ height: 100%; }
+
+main {
+ margin: auto; }
+ @media (min-width: 672px) {
+ main {
+ width: 608px; } }
+ @media (min-width: 800px) {
+ main {
+ width: 736px; } }
+ main section {
+ margin-bottom: 41px; }
+
+.section-title {
+ color: #6E707E;
+ font-size: 13px;
+ font-weight: bold;
+ text-transform: uppercase;
+ margin: 0 0 18px; }
+
+.top-sites-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ margin-inline-end: -32px; }
+ @media (min-width: 672px) {
+ .top-sites-list {
+ width: 640px; } }
+ @media (min-width: 800px) {
+ .top-sites-list {
+ width: 768px; } }
+ .top-sites-list li {
+ display: inline-block;
+ margin: 0 0 18px;
+ margin-inline-end: 32px; }
+ .top-sites-list a {
+ display: block;
+ color: inherit; }
+ .top-sites-list .tile {
+ position: relative;
+ height: 96px;
+ width: 96px;
+ border-radius: 6px;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
+ color: #A0A0A0;
+ font-weight: 200;
+ font-size: 32px;
+ text-transform: uppercase;
+ display: flex;
+ align-items: center;
+ justify-content: center; }
+ .top-sites-list .tile:hover {
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
+ transition: box-shadow 150ms; }
+ .top-sites-list .screenshot {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ background-color: #FFF;
+ border-radius: 6px;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
+ background-size: 250%;
+ background-position: top left;
+ transition: opacity 1s;
+ opacity: 0; }
+ .top-sites-list .screenshot.active {
+ opacity: 1; }
+ .top-sites-list .title {
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ white-space: nowrap;
+ font-size: 11px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 96px; }
+
+.search-wrapper {
+ cursor: default;
+ display: flex;
+ position: relative;
+ margin: 0 0 48px;
+ width: 100%;
+ height: 36px; }
+ .search-wrapper .search-container {
+ z-index: 1001;
+ background: #FFF;
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 100%;
+ margin-top: -2px;
+ border: 1px solid #BFBFBF;
+ font-size: 12px;
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
+ overflow: hidden; }
+ .search-wrapper .search-container .search-title {
+ color: #666;
+ padding: 5px 10px;
+ background-color: #F7F7F7;
+ display: flex;
+ align-items: center;
+ word-break: break-all; }
+ .search-wrapper .search-container .search-title p {
+ margin: 0; }
+ .search-wrapper .search-container .search-title #current-engine-icon {
+ margin-inline-end: 8px; }
+ .search-wrapper .search-container section {
+ border-bottom: 1px solid #EAEAEA;
+ margin-bottom: 0; }
+ .search-wrapper .search-container .search-suggestions ul {
+ padding: 0;
+ margin: 0;
+ list-style: none; }
+ .search-wrapper .search-container .search-suggestions ul li a {
+ cursor: default;
+ color: #000;
+ display: block;
+ padding: 4px 36px; }
+ .search-wrapper .search-container .search-suggestions ul li a:hover, .search-wrapper .search-container .search-suggestions ul li a.active {
+ background: #0996F8;
+ color: #FFF; }
+ .search-wrapper .search-container .history-search-suggestions {
+ border-bottom: 0; }
+ .search-wrapper .search-container .history-search-suggestions ul {
+ padding: 0;
+ margin: 0;
+ list-style: none; }
+ .search-wrapper .search-container .history-search-suggestions ul li a {
+ cursor: default;
+ color: #000;
+ display: block;
+ padding: 4px 10px; }
+ .search-wrapper .search-container .history-search-suggestions ul li a:hover, .search-wrapper .search-container .history-search-suggestions ul li a.active {
+ background: #0996F8;
+ color: #FFF; }
+ .search-wrapper .search-container .history-search-suggestions ul li a:hover > #historyIcon,
+ .search-wrapper .search-container .history-search-suggestions ul li a.active > #historyIcon {
+ background-image: url("assets/glyph-search-history.svg#search-history-active"); }
+ .search-wrapper .search-container .history-search-suggestions #historyIcon {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ margin-inline-end: 10px;
+ margin-bottom: -3px;
+ background-image: url("assets/glyph-search-history.svg#search-history"); }
+ .search-wrapper .search-container .search-partners ul {
+ padding: 0;
+ margin: 0;
+ list-style: none; }
+ .search-wrapper .search-container .search-partners ul li {
+ display: inline-block;
+ padding: 5px 0; }
+ .search-wrapper .search-container .search-partners ul li a {
+ display: block;
+ padding: 3px 16px;
+ border-right: 1px solid #BFBFBF; }
+ .search-wrapper .search-container .search-partners ul li:hover, .search-wrapper .search-container .search-partners ul li.active {
+ background: #0996F8;
+ color: #FFF; }
+ .search-wrapper .search-container .search-partners ul li:hover a, .search-wrapper .search-container .search-partners ul li.active a {
+ border-color: transparent; }
+ .search-wrapper .search-container .search-settings button {
+ color: #666;
+ margin: 0;
+ padding: 0;
+ height: 32px;
+ text-align: center;
+ width: 100%;
+ border-style: solid none none;
+ border-radius: 0;
+ background: #F7F7F7;
+ border-top: 0; }
+ .search-wrapper .search-container .search-settings button:hover, .search-wrapper .search-container .search-settings button.active {
+ background: #EBEBEB;
+ box-shadow: none; }
+ .search-wrapper input {
+ border: 0;
+ box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
+ flex-grow: 1;
+ margin: 0;
+ outline: none;
+ padding: 0 12px 0 35px;
+ height: 100%;
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ padding-inline-start: 35px; }
+ .search-wrapper input:focus {
+ border-color: #0996F8;
+ box-shadow: 0 0 0 2px #0996F8;
+ transition: box-shadow 150ms;
+ z-index: 1; }
+ .search-wrapper input:focus + button {
+ z-index: 1;
+ transition: box-shadow 150ms;
+ box-shadow: 0 0 0 2px #0996F8;
+ background-color: #0996F8;
+ background-image: url("assets/glyph-forward-16-white.svg");
+ color: #FFF; }
+ .search-wrapper input:dir(rtl) {
+ border-radius: 0 4px 4px 0; }
+ .search-wrapper .search-label {
+ background: url("assets/glyph-search-16.svg") no-repeat center center/20px;
+ position: absolute;
+ top: 0;
+ offset-inline-start: 0;
+ height: 100%;
+ width: 35px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2; }
+ .search-wrapper button {
+ border-radius: 0 3px 3px 0;
+ margin-inline-start: -1px;
+ border: 0;
+ width: 36px;
+ padding: 0;
+ transition: box-shadow 150ms;
+ box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
+ background: #FFF url("assets/glyph-forward-16.svg") no-repeat center center;
+ background-size: 16px 16px; }
+ .search-wrapper button:hover {
+ z-index: 1;
+ transition: box-shadow 150ms;
+ box-shadow: 0 1px 0 0 rgba(0, 0, 1, 0.5);
+ background-color: #0996F8;
+ background-image: url("assets/glyph-forward-16-white.svg");
+ color: #FFF; }
+ .search-wrapper button:dir(rtl) {
+ transform: scaleX(-1); }
diff --git a/browser/extensions/activity-stream/data/content/activity-stream.html b/browser/extensions/activity-stream/data/content/activity-stream.html
index ffc614d2245d..2cebfbea096a 100644
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -3,29 +3,14 @@
New Tab
+
-
-
-
+
+
+
+
+
+
+
diff --git a/browser/extensions/activity-stream/data/content/assets/glyph-forward-16-white.svg b/browser/extensions/activity-stream/data/content/assets/glyph-forward-16-white.svg
new file mode 100644
index 000000000000..5bb5b8d535fe
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-forward-16-white.svg
@@ -0,0 +1,7 @@
+
diff --git a/browser/extensions/activity-stream/data/content/assets/glyph-forward-16.svg b/browser/extensions/activity-stream/data/content/assets/glyph-forward-16.svg
new file mode 100644
index 000000000000..4fb2a7e5c9cd
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-forward-16.svg
@@ -0,0 +1,7 @@
+
diff --git a/browser/extensions/activity-stream/data/content/assets/glyph-search-16.svg b/browser/extensions/activity-stream/data/content/assets/glyph-search-16.svg
new file mode 100644
index 000000000000..4d5eddbbe081
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-search-16.svg
@@ -0,0 +1,13 @@
+
diff --git a/browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg b/browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
new file mode 100644
index 000000000000..1c04b429f791
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
@@ -0,0 +1,19 @@
+
+
diff --git a/browser/extensions/activity-stream/jar.mn b/browser/extensions/activity-stream/jar.mn
index 186d65e4a4ed..8546dcb750f9 100644
--- a/browser/extensions/activity-stream/jar.mn
+++ b/browser/extensions/activity-stream/jar.mn
@@ -7,4 +7,8 @@
content/lib/ (./lib/*)
content/common/ (./common/*)
content/vendor/Redux.jsm (./vendor/Redux.jsm)
+ content/vendor/react.js (./vendor/react.js)
+ content/vendor/react-dom.js (./vendor/react-dom.js)
+ content/vendor/redux.js (./vendor/redux.js)
+ content/vendor/react-redux.js (./vendor/react-redux.js)
content/data/ (./data/*)
diff --git a/browser/extensions/activity-stream/lib/ActivityStream.jsm b/browser/extensions/activity-stream/lib/ActivityStream.jsm
index ac0f92d9875b..18035bdf27b6 100644
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -1,13 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* globals XPCOMUtils, NewTabInit, TopSitesFeed, SearchFeed */
+
"use strict";
const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
+const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
// Feeds
-const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm", {});
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabInit",
+ "resource://activity-stream/lib/NewTabInit.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TopSitesFeed",
+ "resource://activity-stream/lib/TopSitesFeed.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SearchFeed",
+ "resource://activity-stream/lib/SearchFeed.jsm");
+
+const feeds = {
+ // When you add a feed here:
+ // 1. The key in this object should directly refer to a pref, not including the
+ // prefix (so "feeds.newtabinit" refers to the
+ // "browser.newtabpage.activity-stream.feeds.newtabinit" pref)
+ // 2. The value should be a function that returns a feed.
+ // 3. You should use XPCOMUtils.defineLazyModuleGetter to import the Feed,
+ // so it isn't loaded until the feed is enabled.
+ "feeds.newtabinit": () => new NewTabInit(),
+ "feeds.topsites": () => new TopSitesFeed(),
+ "feeds.search": () => new SearchFeed()
+};
this.ActivityStream = class ActivityStream {
@@ -23,14 +45,15 @@ this.ActivityStream = class ActivityStream {
this.initialized = false;
this.options = options;
this.store = new Store();
+ this.feeds = feeds;
}
init() {
this.initialized = true;
- this.store.init([
- new NewTabInit()
- ]);
+ this.store.init(this.feeds);
+ this.store.dispatch({type: at.INIT});
}
uninit() {
+ this.store.dispatch({type: at.UNINIT});
this.store.uninit();
this.initialized = false;
}
diff --git a/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
index 70cdc354fd32..c6ce0f7930ca 100644
--- a/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
@@ -33,6 +33,7 @@ const DEFAULT_OPTIONS = {
};
this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
+
/**
* ActivityStreamMessageChannel - This module connects a Redux store to a RemotePageManager in Firefox.
* Call .createChannel to start the connection, and .destroyChannel to destroy it.
@@ -183,13 +184,17 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
* @param {obj} msg.target A message target
*/
onMessage(msg) {
- const action = msg.data;
const {portID} = msg.target;
- if (!action || !action.type) {
+ if (!msg.data || !msg.data.type) {
Cu.reportError(new Error(`Received an improperly formatted message from ${portID}`));
return;
}
- this.onActionFromContent(action, msg.target.portID);
+ let action = {};
+ Object.assign(action, msg.data);
+ // target is used to access a browser reference that came from the content
+ // and should only be used in feeds (not reducers)
+ action._target = msg.target;
+ this.onActionFromContent(action, portID);
}
}
diff --git a/browser/extensions/activity-stream/lib/SearchFeed.jsm b/browser/extensions/activity-stream/lib/SearchFeed.jsm
new file mode 100644
index 000000000000..992c07f69384
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/SearchFeed.jsm
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ /* globals ContentSearch, XPCOMUtils, Services */
+"use strict";
+
+const {utils: Cu} = Components;
+const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
+ "resource:///modules/ContentSearch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+this.SearchFeed = class SearchFeed {
+ addObservers() {
+ Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC);
+ }
+ removeObservers() {
+ Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
+ }
+ observe(subject, topic, data) {
+ switch (topic) {
+ case SEARCH_ENGINE_TOPIC:
+ if (data !== "engine-default") {
+ this.getState();
+ }
+ break;
+ }
+ }
+ async getState() {
+ const state = await ContentSearch.currentStateObj(true);
+ const engines = state.engines.map(engine => ({
+ name: engine.name,
+ icon: engine.iconBuffer
+ }));
+ const currentEngine = {
+ name: state.currentEngine.name,
+ icon: state.currentEngine.iconBuffer
+ };
+ const action = {type: at.SEARCH_STATE_UPDATED, data: {engines, currentEngine}};
+ this.store.dispatch(ac.BroadcastToContent(action));
+ }
+ performSearch(browser, data) {
+ ContentSearch.performSearch({target: browser}, data);
+ }
+ onAction(action) {
+ switch (action.type) {
+ case at.INIT:
+ this.addObservers();
+ this.getState();
+ break;
+ case at.PERFORM_SEARCH:
+ this.performSearch(action._target.browser, action.data);
+ break;
+ case at.UNINIT:
+ this.removeObservers();
+ break;
+ }
+ }
+};
+this.EXPORTED_SYMBOLS = ["SearchFeed"];
diff --git a/browser/extensions/activity-stream/lib/Store.jsm b/browser/extensions/activity-stream/lib/Store.jsm
index 196ce16df230..9bac91a0207c 100644
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -1,15 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global Preferences */
"use strict";
const {utils: Cu} = Components;
const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
-const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
+const PREF_PREFIX = "browser.newtabpage.activity-stream.";
+Cu.import("resource://gre/modules/Preferences.jsm");
+
/**
* Store - This has a similar structure to a redux store, but includes some extra
* functionality to allow for routing of actions between the Main processes
@@ -32,7 +35,9 @@ this.Store = class Store {
return this._store[method](...args);
}.bind(this);
});
- this.feeds = new Set();
+ this.feeds = new Map();
+ this._feedFactories = null;
+ this._prefHandlers = new Map();
this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
this._store = redux.createStore(
redux.combineReducers(reducers),
@@ -53,33 +58,93 @@ this.Store = class Store {
}
/**
- * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
- * After initialization has finished, an INIT action is dispatched.
+ * initFeed - Initializes a feed by calling its constructor function
*
- * @param {array} feeds An array of objects with an optional .onAction method
+ * @param {string} feedName The name of a feed, as defined in the object
+ * passed to Store.init
*/
- init(feeds) {
- if (feeds) {
- feeds.forEach(subscriber => {
- subscriber.store = this;
- this.feeds.add(subscriber);
- });
- }
- this._messageChannel.createChannel();
- this.dispatch({type: at.INIT});
+ initFeed(feedName) {
+ const feed = this._feedFactories[feedName]();
+ feed.store = this;
+ this.feeds.set(feedName, feed);
}
/**
- * uninit - Clears all feeds, dispatches an UNINIT action, and
- * destroys the message manager channel.
+ * uninitFeed - Removes a feed and calls its uninit function if defined
+ *
+ * @param {string} feedName The name of a feed, as defined in the object
+ * passed to Store.init
+ */
+ uninitFeed(feedName) {
+ const feed = this.feeds.get(feedName);
+ if (!feed) {
+ return;
+ }
+ if (feed.uninit) {
+ feed.uninit();
+ }
+ this.feeds.delete(feedName);
+ }
+
+ /**
+ * maybeStartFeedAndListenForPrefChanges - Listen for pref changes that turn a
+ * feed off/on, and as long as that pref was not explicitly set to
+ * false, initialize the feed immediately.
+ *
+ * @param {string} name The name of a feed, as defined in the object passed
+ * to Store.init
+ */
+ maybeStartFeedAndListenForPrefChanges(name) {
+ const prefName = PREF_PREFIX + name;
+
+ // If the pref was never set, set it to true by default.
+ if (!Preferences.has(prefName)) {
+ Preferences.set(prefName, true);
+ }
+
+ // Create a listener that turns the feed off/on based on changes
+ // to the pref, and cache it so we can unlisten on shut-down.
+ const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(name) : this.uninitFeed(name));
+ this._prefHandlers.set(prefName, onPrefChanged);
+ Preferences.observe(prefName, onPrefChanged);
+
+ // TODO: This should propbably be done in a generic pref manager for Activity Stream.
+ // If the pref is true, start the feed immediately.
+ if (Preferences.get(prefName)) {
+ this.initFeed(name);
+ }
+ }
+
+ /**
+ * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
+ *
+ * @param {array} feeds An array of objects with an optional .onAction method
+ */
+ init(feedConstructors) {
+ if (feedConstructors) {
+ this._feedFactories = feedConstructors;
+ for (const name of Object.keys(feedConstructors)) {
+ this.maybeStartFeedAndListenForPrefChanges(name);
+ }
+ }
+ this._messageChannel.createChannel();
+ }
+
+ /**
+ * uninit - Uninitalizes each feed, clears them, and destroys the message
+ * manager channel.
*
* @return {type} description
*/
uninit() {
+ this.feeds.forEach(feed => this.uninitFeed(feed));
+ this._prefHandlers.forEach((handler, pref) => Preferences.ignore(pref, handler));
+ this._prefHandlers.clear();
+ this._feedFactories = null;
this.feeds.clear();
- this.dispatch({type: at.UNINIT});
this._messageChannel.destroyChannel();
}
};
-this.EXPORTED_SYMBOLS = ["Store"];
+this.PREF_PREFIX = PREF_PREFIX;
+this.EXPORTED_SYMBOLS = ["Store", "PREF_PREFIX"];
diff --git a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
new file mode 100644
index 000000000000..14fc41dd5858
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ /* globals PlacesProvider, PreviewProvider */
+"use strict";
+
+const {utils: Cu} = Components;
+const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+
+Cu.import("resource:///modules/PlacesProvider.jsm");
+Cu.import("resource:///modules/PreviewProvider.jsm");
+
+const TOP_SITES_SHOWMORE_LENGTH = 12;
+const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
+const DEFAULT_TOP_SITES = [
+ {"url": "https://www.facebook.com/"},
+ {"url": "https://www.youtube.com/"},
+ {"url": "http://www.amazon.com/"},
+ {"url": "https://www.yahoo.com/"},
+ {"url": "http://www.ebay.com"},
+ {"url": "https://twitter.com/"}
+].map(row => Object.assign(row, {isDefault: true}));
+
+this.TopSitesFeed = class TopSitesFeed {
+ constructor() {
+ this.lastUpdated = 0;
+ }
+ async getScreenshot(url) {
+ let screenshot = await PreviewProvider.getThumbnail(url);
+ const action = {type: at.SCREENSHOT_UPDATED, data: {url, screenshot}};
+ this.store.dispatch(ac.BroadcastToContent(action));
+ }
+ async getLinksWithDefaults(action) {
+ let links = await PlacesProvider.links.getLinks();
+
+ if (!links) {
+ links = [];
+ } else {
+ links = links.filter(link => link && link.type !== "affiliate").slice(0, 12);
+ }
+
+ if (links.length < TOP_SITES_SHOWMORE_LENGTH) {
+ links = [...links, ...DEFAULT_TOP_SITES].slice(0, TOP_SITES_SHOWMORE_LENGTH);
+ }
+
+ return links;
+ }
+ async refresh(action) {
+ const links = await this.getLinksWithDefaults();
+ const newAction = {type: at.TOP_SITES_UPDATED, data: links};
+
+ // Send an update to content so the preloaded tab can get the updated content
+ this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget));
+ this.lastUpdated = Date.now();
+
+ // Now, get a screenshot for every item
+ for (let link of links) {
+ this.getScreenshot(link.url);
+ }
+ }
+ onAction(action) {
+ let realRows;
+ switch (action.type) {
+ case at.NEW_TAB_LOAD:
+ // Only check against real rows returned from history, not default ones.
+ realRows = this.store.getState().TopSites.rows.filter(row => !row.isDefault);
+ // When a new tab is opened, if we don't have enough top sites yet, refresh the data.
+ if (realRows.length < TOP_SITES_SHOWMORE_LENGTH) {
+ this.refresh(action);
+ } else if (Date.now() - this.lastUpdated >= UPDATE_TIME) {
+ // When a new tab is opened, if the last time we refreshed the data
+ // is greater than 15 minutes, refresh the data.
+ this.refresh(action);
+ }
+ break;
+ }
+ }
+};
+
+this.UPDATE_TIME = UPDATE_TIME;
+this.TOP_SITES_SHOWMORE_LENGTH = TOP_SITES_SHOWMORE_LENGTH;
+this.DEFAULT_TOP_SITES = DEFAULT_TOP_SITES;
+this.EXPORTED_SYMBOLS = ["TopSitesFeed", "UPDATE_TIME", "DEFAULT_TOP_SITES", "TOP_SITES_SHOWMORE_LENGTH"];
diff --git a/browser/extensions/activity-stream/test/.eslintrc.js b/browser/extensions/activity-stream/test/.eslintrc.js
new file mode 100644
index 000000000000..438e41b01a3c
--- /dev/null
+++ b/browser/extensions/activity-stream/test/.eslintrc.js
@@ -0,0 +1,11 @@
+module.exports = {
+ "env": {
+ "node": true,
+ "es6": true,
+ "mocha": true
+ },
+ "globals": {
+ "assert": true,
+ "sinon": true
+ }
+};
diff --git a/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js b/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
new file mode 100644
index 000000000000..b1148a7004e8
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
@@ -0,0 +1,42 @@
+module.exports = {
+ "globals": {
+ "add_task": false,
+ "Assert": false,
+ "BrowserOpenTab": false,
+ "BrowserTestUtils": false,
+ "content": false,
+ "ContentTask": false,
+ "ContentTaskUtils": false,
+ "Components": false,
+ "EventUtils": false,
+ "executeSoon": false,
+ "expectUncaughtException": false,
+ "export_assertions": false,
+ "extractJarToTmp": false,
+ "finish": false,
+ "getJar": false,
+ "getRootDirectory": false,
+ "getTestFilePath": false,
+ "gBrowser": false,
+ "gTestPath": false,
+ "info": false,
+ "is": false,
+ "isnot": false,
+ "ok": false,
+ "OpenBrowserWindow": false,
+ "Preferences": false,
+ "registerCleanupFunction": false,
+ "requestLongerTimeout": false,
+ "Services": false,
+ "SimpleTest": false,
+ "SpecialPowers": false,
+ "TestUtils": false,
+ "thisTestLeaksUncaughtRejectionsAndShouldBeFixed": false,
+ "todo": false,
+ "todo_is": false,
+ "todo_isnot": false,
+ "waitForClipboard": false,
+ "waitForExplicitFinish": false,
+ "waitForFocus": false
+ }
+};
diff --git a/browser/extensions/activity-stream/test/functional/mochitest/browser.ini b/browser/extensions/activity-stream/test/functional/mochitest/browser.ini
new file mode 100644
index 000000000000..65e4d43db9c8
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+# XXX This defaults to forcing activity-stream tests to be skipped in m-c,
+# since, as of this writing, mozilla-central itself is still turned off.
+# The tests can be run locally using 'npm run mochitest' which does various
+# overrides.
+skip-if=!activity_stream
+
+[browser_dummy_test.js]
+skip-if=true
+# XXX The above test is required because having only one test causes
+# The default skip-if to silently fail. As soon as we add another test here,
+# we should get rid of it, and the following line.
+[browser_as_load_location.js]
diff --git a/browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js b/browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js
new file mode 100644
index 000000000000..7106aa624fc6
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js
@@ -0,0 +1,34 @@
+"use strict";
+
+let Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Tests that opening a new tab opens a page with the expected activity stream
+ * content.
+ *
+ * XXX /browser/components/newtab/tests/browser/browser_newtab_overrides in
+ * mozilla-central is where this test was adapted from. Once we get decide on
+ * and implement how we're going to set the URL in mozilla-central, we may well
+ * want to (separately from this test), clone/adapt that entire file for our
+ * new setup.
+ */
+add_task(async function checkActivityStreamLoads() {
+ const asURL = "resource://activity-stream/data/content/activity-stream.html";
+
+ // simulate a newtab open as a user would
+ BrowserOpenTab();
+
+ // wait until the browser loads
+ let browser = gBrowser.selectedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // check what the content task thinks has been loaded.
+ await ContentTask.spawn(browser, {url: asURL}, args => {
+ Assert.ok(content.document.querySelector("body.activity-stream"),
+ 'Got {
+ Assert.ok(content.document.querySelector("body.activity-stream"),
+ 'Got {
+ describe("_RouteMessage", () => {
+ it("should throw if options are not passed as the second param", () => {
+ assert.throws(() => {
+ au._RouteMessage({type: "FOO"});
+ });
+ });
+ it("should set all defined options on the .meta property of the new action", () => {
+ assert.deepEqual(
+ au._RouteMessage({type: "FOO", meta: {hello: "world"}}, {from: "foo", to: "bar"}),
+ {type: "FOO", meta: {hello: "world", from: "foo", to: "bar"}}
+ );
+ });
+ it("should remove any undefined options related to message routing", () => {
+ const action = au._RouteMessage({type: "FOO", meta: {fromTarget: "bar"}}, {from: "foo", to: "bar"});
+ assert.isUndefined(action.meta.fromTarget);
+ });
+ });
+ describe("SendToMain", () => {
+ it("should create the right action", () => {
+ const action = {type: "FOO", data: "BAR"};
+ const newAction = ac.SendToMain(action);
+ assert.deepEqual(newAction, {
+ type: "FOO",
+ data: "BAR",
+ meta: {from: CONTENT_MESSAGE_TYPE, to: MAIN_MESSAGE_TYPE}
+ });
+ });
+ describe("isSendToMain", () => {
+ it("should return true if action is SendToMain", () => {
+ const newAction = ac.SendToMain({type: "FOO"});
+ assert.isTrue(au.isSendToMain(newAction));
+ });
+ it("should return false if action is not SendToMain", () => {
+ assert.isFalse(au.isSendToMain({type: "FOO"}));
+ });
+ });
+ });
+ describe("SendToContent", () => {
+ it("should create the right action", () => {
+ const action = {type: "FOO", data: "BAR"};
+ const targetId = "abc123";
+ const newAction = ac.SendToContent(action, targetId);
+ assert.deepEqual(newAction, {
+ type: "FOO",
+ data: "BAR",
+ meta: {from: MAIN_MESSAGE_TYPE, to: CONTENT_MESSAGE_TYPE, toTarget: targetId}
+ });
+ });
+ it("should throw if no targetId is provided", () => {
+ assert.throws(() => {
+ ac.SendToContent({type: "FOO"});
+ });
+ });
+ describe("isSendToContent", () => {
+ it("should return true if action is SendToContent", () => {
+ const newAction = ac.SendToContent({type: "FOO"}, "foo123");
+ assert.isTrue(au.isSendToContent(newAction));
+ });
+ it("should return false if action is not SendToMain", () => {
+ assert.isFalse(au.isSendToContent({type: "FOO"}));
+ assert.isFalse(au.isSendToContent(ac.BroadcastToContent({type: "FOO"})));
+ });
+ });
+ });
+ describe("BroadcastToContent", () => {
+ it("should create the right action", () => {
+ const action = {type: "FOO", data: "BAR"};
+ const newAction = ac.BroadcastToContent(action);
+ assert.deepEqual(newAction, {
+ type: "FOO",
+ data: "BAR",
+ meta: {from: MAIN_MESSAGE_TYPE, to: CONTENT_MESSAGE_TYPE}
+ });
+ });
+ describe("isBroadcastToContent", () => {
+ it("should return true if action is BroadcastToContent", () => {
+ assert.isTrue(au.isBroadcastToContent(ac.BroadcastToContent({type: "FOO"})));
+ });
+ it("should return false if action is not BroadcastToContent", () => {
+ assert.isFalse(au.isBroadcastToContent({type: "FOO"}));
+ assert.isFalse(au.isBroadcastToContent(ac.SendToContent({type: "FOO"}, "foo123")));
+ });
+ });
+ });
+});
diff --git a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
new file mode 100644
index 000000000000..29e995a4e8ce
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -0,0 +1,51 @@
+const {reducers, INITIAL_STATE} = require("common/Reducers.jsm");
+const {TopSites, Search} = reducers;
+const {actionTypes: at} = require("common/Actions.jsm");
+
+describe("Reducers", () => {
+ describe("TopSites", () => {
+ it("should return the initial state", () => {
+ const nextState = TopSites(undefined, {type: "FOO"});
+ assert.equal(nextState, INITIAL_STATE.TopSites);
+ });
+ it("should add top sites on TOP_SITES_UPDATED", () => {
+ const newRows = [{url: "foo.com"}, {url: "bar.com"}];
+ const nextState = TopSites(undefined, {type: at.TOP_SITES_UPDATED, data: newRows});
+ assert.equal(nextState.rows, newRows);
+ });
+ it("should not update state for empty action.data on TOP_SITES_UPDATED", () => {
+ const nextState = TopSites(undefined, {type: at.TOP_SITES_UPDATED});
+ assert.equal(nextState, INITIAL_STATE.TopSites);
+ });
+ it("should add screenshots for SCREENSHOT_UPDATED", () => {
+ const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
+ const action = {type: at.SCREENSHOT_UPDATED, data: {url: "bar.com", screenshot: "data:123"}};
+ const nextState = TopSites(oldState, action);
+ assert.deepEqual(nextState.rows, [{url: "foo.com"}, {url: "bar.com", screenshot: "data:123"}]);
+ });
+ it("should not modify rows if nothing matches the url for SCREENSHOT_UPDATED", () => {
+ const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
+ const action = {type: at.SCREENSHOT_UPDATED, data: {url: "baz.com", screenshot: "data:123"}};
+ const nextState = TopSites(oldState, action);
+ assert.deepEqual(nextState, oldState);
+ });
+ });
+ describe("Search", () => {
+ it("should return the initial state", () => {
+ const nextState = Search(undefined, {type: "FOO"});
+ assert.equal(nextState, INITIAL_STATE.Search);
+ });
+ it("should not update state for empty action.data on Search", () => {
+ const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED});
+ assert.equal(nextState, INITIAL_STATE.Search);
+ });
+ it("should update the current engine and the engines on SEARCH_STATE_UPDATED", () => {
+ const newEngine = {name: "Google", iconBuffer: "icon.ico"};
+ const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED, data: {currentEngine: newEngine, engines: [newEngine]}});
+ assert.equal(nextState.currentEngine.name, newEngine.name);
+ assert.equal(nextState.currentEngine.icon, newEngine.icon);
+ assert.equal(nextState.engines[0].name, newEngine.name);
+ assert.equal(nextState.engines[0].icon, newEngine.icon);
+ });
+ });
+});
diff --git a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
new file mode 100644
index 000000000000..7a3bada159e8
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -0,0 +1,70 @@
+const injector = require("inject!lib/ActivityStream.jsm");
+
+describe("ActivityStream", () => {
+ let sandbox;
+ let as;
+ let ActivityStream;
+ function NewTabInit() {}
+ function TopSitesFeed() {}
+ function SearchFeed() {}
+ before(() => {
+ sandbox = sinon.sandbox.create();
+ ({ActivityStream} = injector({
+ "lib/NewTabInit.jsm": {NewTabInit},
+ "lib/TopSitesFeed.jsm": {TopSitesFeed},
+ "lib/SearchFeed.jsm": {SearchFeed}
+ }));
+ });
+
+ afterEach(() => sandbox.restore());
+
+ beforeEach(() => {
+ as = new ActivityStream();
+ sandbox.stub(as.store, "init");
+ sandbox.stub(as.store, "uninit");
+ });
+
+ it("should exist", () => {
+ assert.ok(ActivityStream);
+ });
+ it("should initialize with .initialized=false", () => {
+ assert.isFalse(as.initialized, ".initialized");
+ });
+ describe("#init", () => {
+ beforeEach(() => {
+ as.init();
+ });
+ it("should set .initialized to true", () => {
+ assert.isTrue(as.initialized, ".initialized");
+ });
+ it("should call .store.init", () => {
+ assert.calledOnce(as.store.init);
+ });
+ });
+ describe("#uninit", () => {
+ beforeEach(() => {
+ as.init();
+ as.uninit();
+ });
+ it("should set .initialized to false", () => {
+ assert.isFalse(as.initialized, ".initialized");
+ });
+ it("should call .store.uninit", () => {
+ assert.calledOnce(as.store.uninit);
+ });
+ });
+ describe("feeds", () => {
+ it("should create a NewTabInit feed", () => {
+ const feed = as.feeds["feeds.newtabinit"]();
+ assert.instanceOf(feed, NewTabInit);
+ });
+ it("should create a TopSites feed", () => {
+ const feed = as.feeds["feeds.topsites"]();
+ assert.instanceOf(feed, TopSitesFeed);
+ });
+ it("should create a Search feed", () => {
+ const feed = as.feeds["feeds.search"]();
+ assert.instanceOf(feed, SearchFeed);
+ });
+ });
+});
diff --git a/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
new file mode 100644
index 000000000000..a5a48f74fc5c
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
@@ -0,0 +1,235 @@
+const {ActivityStreamMessageChannel, DEFAULT_OPTIONS} = require("lib/ActivityStreamMessageChannel.jsm");
+const {addNumberReducer, GlobalOverrider} = require("test/unit/utils");
+const {createStore, applyMiddleware} = require("redux");
+const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
+
+const OPTIONS = ["pageURL, outgoingMessageName", "incomingMessageName", "dispatch"];
+
+describe("ActivityStreamMessageChannel", () => {
+ let globals;
+ let dispatch;
+ let mm;
+ before(() => {
+ function RP(url) {
+ this.url = url;
+ this.messagePorts = [];
+ this.addMessageListener = globals.sandbox.spy();
+ this.sendAsyncMessage = globals.sandbox.spy();
+ this.destroy = globals.sandbox.spy();
+ }
+ globals = new GlobalOverrider();
+ globals.set("AboutNewTab", {
+ override: globals.sandbox.spy(),
+ reset: globals.sandbox.spy()
+ });
+ globals.set("RemotePages", RP);
+ dispatch = globals.sandbox.spy();
+ });
+ beforeEach(() => {
+ mm = new ActivityStreamMessageChannel({dispatch});
+ });
+
+ afterEach(() => globals.reset());
+ after(() => globals.restore());
+
+ it("should exist", () => {
+ assert.ok(ActivityStreamMessageChannel);
+ });
+ it("should apply default options", () => {
+ mm = new ActivityStreamMessageChannel();
+ OPTIONS.forEach(o => assert.equal(mm[o], DEFAULT_OPTIONS[o], o));
+ });
+ it("should add options", () => {
+ const options = {dispatch: () => {}, pageURL: "FOO.html", outgoingMessageName: "OUT", incomingMessageName: "IN"};
+ mm = new ActivityStreamMessageChannel(options);
+ OPTIONS.forEach(o => assert.equal(mm[o], options[o], o));
+ });
+ it("should throw an error if no dispatcher was provided", () => {
+ mm = new ActivityStreamMessageChannel();
+ assert.throws(() => mm.dispatch({type: "FOO"}));
+ });
+ describe("Creating/destroying the channel", () => {
+ describe("#createChannel", () => {
+ it("should create .channel with the correct URL", () => {
+ mm.createChannel();
+ assert.ok(mm.channel);
+ assert.equal(mm.channel.url, mm.pageURL);
+ });
+ it("should add 3 message listeners", () => {
+ mm.createChannel();
+ assert.callCount(mm.channel.addMessageListener, 3);
+ });
+ it("should add the custom message listener to the channel", () => {
+ mm.createChannel();
+ assert.calledWith(mm.channel.addMessageListener, mm.incomingMessageName, mm.onMessage);
+ });
+ it("should override AboutNewTab", () => {
+ mm.createChannel();
+ assert.calledOnce(global.AboutNewTab.override);
+ });
+ it("should not override AboutNewTab if the pageURL is not about:newtab", () => {
+ mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
+ mm.createChannel();
+ assert.notCalled(global.AboutNewTab.override);
+ });
+ });
+ describe("#destroyChannel", () => {
+ let channel;
+ beforeEach(() => {
+ mm.createChannel();
+ channel = mm.channel;
+ });
+ it("should call channel.destroy()", () => {
+ mm.destroyChannel();
+ assert.calledOnce(channel.destroy);
+ });
+ it("should set .channel to null", () => {
+ mm.destroyChannel();
+ assert.isNull(mm.channel);
+ });
+ it("should reset AboutNewTab", () => {
+ mm.destroyChannel();
+ assert.calledOnce(global.AboutNewTab.reset);
+ });
+ it("should not reset AboutNewTab if the pageURL is not about:newtab", () => {
+ mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
+ mm.createChannel();
+ mm.destroyChannel();
+ assert.notCalled(global.AboutNewTab.reset);
+ });
+ });
+ });
+ describe("Message handling", () => {
+ describe("#getTargetById", () => {
+ it("should get an id if it exists", () => {
+ const t = {portID: "foo"};
+ mm.createChannel();
+ mm.channel.messagePorts.push(t);
+ assert.equal(mm.getTargetById("foo"), t);
+ });
+ it("should return null if the target doesn't exist", () => {
+ const t = {portID: "foo"};
+ mm.createChannel();
+ mm.channel.messagePorts.push(t);
+ assert.equal(mm.getTargetById("bar"), null);
+ });
+ });
+ describe("#onNewTabLoad", () => {
+ it("should dispatch a NEW_TAB_LOAD action", () => {
+ const t = {portID: "foo"};
+ sinon.stub(mm, "onActionFromContent");
+ mm.onNewTabLoad({target: t});
+ assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_LOAD}, "foo");
+ });
+ });
+ describe("#onNewTabUnload", () => {
+ it("should dispatch a NEW_TAB_UNLOAD action", () => {
+ const t = {portID: "foo"};
+ sinon.stub(mm, "onActionFromContent");
+ mm.onNewTabUnload({target: t});
+ assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_UNLOAD}, "foo");
+ });
+ });
+ describe("#onMessage", () => {
+ it("should report an error if the msg.data is missing", () => {
+ mm.onMessage({target: {portID: "foo"}});
+ assert.calledOnce(global.Components.utils.reportError);
+ });
+ it("should report an error if the msg.data.type is missing", () => {
+ mm.onMessage({target: {portID: "foo"}, data: "foo"});
+ assert.calledOnce(global.Components.utils.reportError);
+ });
+ it("should call onActionFromContent", () => {
+ sinon.stub(mm, "onActionFromContent");
+ const action = {data: {data: {}, type: "FOO"}, target: {portID: "foo"}};
+ const expectedAction = {
+ type: action.data.type,
+ data: action.data.data,
+ _target: {portID: "foo"}
+ };
+ mm.onMessage(action);
+ assert.calledWith(mm.onActionFromContent, expectedAction, "foo");
+ });
+ });
+ });
+ describe("Sending and broadcasting", () => {
+ describe("#send", () => {
+ it("should send a message on the right port", () => {
+ const t = {portID: "foo", sendAsyncMessage: sinon.spy()};
+ mm.createChannel();
+ mm.channel.messagePorts = [t];
+ const action = ac.SendToContent({type: "HELLO"}, "foo");
+ mm.send(action, "foo");
+ assert.calledWith(t.sendAsyncMessage, DEFAULT_OPTIONS.outgoingMessageName, action);
+ });
+ it("should not throw if the target isn't around", () => {
+ mm.createChannel();
+ // port is not added to the channel
+ const action = ac.SendToContent({type: "HELLO"}, "foo");
+
+ assert.doesNotThrow(() => mm.send(action, "foo"));
+ });
+ });
+ describe("#broadcast", () => {
+ it("should send a message on the channel", () => {
+ mm.createChannel();
+ const action = ac.BroadcastToContent({type: "HELLO"});
+ mm.broadcast(action);
+ assert.calledWith(mm.channel.sendAsyncMessage, DEFAULT_OPTIONS.outgoingMessageName, action);
+ });
+ });
+ });
+ describe("Handling actions", () => {
+ describe("#onActionFromContent", () => {
+ beforeEach(() => mm.onActionFromContent({type: "FOO"}, "foo"));
+ it("should dispatch a SendToMain action", () => {
+ assert.calledOnce(dispatch);
+ const action = dispatch.firstCall.args[0];
+ assert.equal(action.type, "FOO", "action.type");
+ });
+ it("should have the right fromTarget", () => {
+ const action = dispatch.firstCall.args[0];
+ assert.equal(action.meta.fromTarget, "foo", "meta.fromTarget");
+ });
+ });
+ describe("#middleware", () => {
+ let store;
+ beforeEach(() => {
+ store = createStore(addNumberReducer, applyMiddleware(mm.middleware));
+ });
+ it("should just call next if no channel is found", () => {
+ store.dispatch({type: "ADD", data: 10});
+ assert.equal(store.getState(), 10);
+ });
+ it("should call .send if the action is SendToContent", () => {
+ sinon.stub(mm, "send");
+ const action = ac.SendToContent({type: "FOO"}, "foo");
+
+ mm.createChannel();
+ store.dispatch(action);
+
+ assert.calledWith(mm.send, action);
+ });
+ it("should call .broadcast if the action is BroadcastToContent", () => {
+ sinon.stub(mm, "broadcast");
+ const action = ac.BroadcastToContent({type: "FOO"});
+
+ mm.createChannel();
+ store.dispatch(action);
+
+ assert.calledWith(mm.broadcast, action);
+ });
+ it("should dispatch other actions normally", () => {
+ sinon.stub(mm, "send");
+ sinon.stub(mm, "broadcast");
+
+ mm.createChannel();
+ store.dispatch({type: "ADD", data: 1});
+
+ assert.equal(store.getState(), 1);
+ assert.notCalled(mm.send);
+ assert.notCalled(mm.broadcast);
+ });
+ });
+ });
+});
diff --git a/browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js b/browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
new file mode 100644
index 000000000000..d1e710cbb221
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
@@ -0,0 +1,77 @@
+"use strict";
+const {SearchFeed} = require("lib/SearchFeed.jsm");
+const {GlobalOverrider} = require("test/unit/utils");
+const {actionTypes: at} = require("common/Actions.jsm");
+const fakeEngines = [{name: "Google", iconBuffer: "icon.ico"}];
+describe("Search Feed", () => {
+ let feed;
+ let globals;
+ before(() => {
+ globals = new GlobalOverrider();
+ globals.set("ContentSearch", {
+ currentStateObj: globals.sandbox.spy(() => Promise.resolve({engines: fakeEngines, currentEngine: {}})),
+ performSearch: globals.sandbox.spy((browser, searchData) => Promise.resolve({browser, searchData}))
+ });
+ });
+ beforeEach(() => {
+ feed = new SearchFeed();
+ feed.store = {dispatch: sinon.spy()};
+ });
+ afterEach(() => globals.reset());
+ after(() => globals.restore());
+
+ it("should call get state (with true) from the content search provider on INIT", () => {
+ feed.onAction({type: at.INIT});
+ // calling currentStateObj with 'true' allows us to return a data uri for the
+ // icon, instead of an array buffer
+ assert.calledWith(global.ContentSearch.currentStateObj, true);
+ });
+ it("should get the the state on INIT", () => {
+ sinon.stub(feed, "getState");
+ feed.onAction({type: at.INIT});
+ assert.calledOnce(feed.getState);
+ });
+ it("should add observers on INIT", () => {
+ sinon.stub(feed, "addObservers");
+ feed.onAction({type: at.INIT});
+ assert.calledOnce(feed.addObservers);
+ });
+ it("should remove observers on UNINIT", () => {
+ sinon.stub(feed, "removeObservers");
+ feed.onAction({type: at.UNINIT});
+ assert.calledOnce(feed.removeObservers);
+ });
+ it("should call services.obs.addObserver on INIT", () => {
+ feed.onAction({type: at.INIT});
+ assert.calledOnce(global.Services.obs.addObserver);
+ });
+ it("should call services.obs.removeObserver on UNINIT", () => {
+ feed.onAction({type: at.UNINIT});
+ assert.calledOnce(global.Services.obs.removeObserver);
+ });
+ it("should dispatch one event with the state", () => (
+ feed.getState().then(() => {
+ assert.calledOnce(feed.store.dispatch);
+ })
+ ));
+ it("should perform a search on PERFORM_SEARCH", () => {
+ sinon.stub(feed, "performSearch");
+ feed.onAction({_target: {browser: {}}, type: at.PERFORM_SEARCH});
+ assert.calledOnce(feed.performSearch);
+ });
+ it("should call performSearch with an action", () => {
+ const action = {_target: {browser: "browser"}, data: {searchString: "hello"}};
+ feed.performSearch(action._target.browser, action.data);
+ assert.calledWith(global.ContentSearch.performSearch, {target: action._target.browser}, action.data);
+ });
+ it("should get the state if we change the search engines", () => {
+ sinon.stub(feed, "getState");
+ feed.observe(null, "browser-search-engine-modified", "engine-current");
+ assert.calledOnce(feed.getState);
+ });
+ it("shouldn't get the state if it's not the right notification", () => {
+ sinon.stub(feed, "getState");
+ feed.observe(null, "some-other-notification", "engine-current");
+ assert.notCalled(feed.getState);
+ });
+});
diff --git a/browser/extensions/activity-stream/test/unit/lib/Store.test.js b/browser/extensions/activity-stream/test/unit/lib/Store.test.js
new file mode 100644
index 000000000000..d49aa3dececa
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/Store.test.js
@@ -0,0 +1,210 @@
+const injector = require("inject!lib/Store.jsm");
+const {createStore} = require("redux");
+const {addNumberReducer} = require("test/unit/utils");
+const {GlobalOverrider} = require("test/unit/utils");
+describe("Store", () => {
+ let Store;
+ let Preferences;
+ let sandbox;
+ let store;
+ let globals;
+ let PREF_PREFIX;
+ beforeEach(() => {
+ globals = new GlobalOverrider();
+ sandbox = globals.sandbox;
+ Preferences = new Map();
+ Preferences.observe = sandbox.spy();
+ Preferences.ignore = sandbox.spy();
+ globals.set("Preferences", Preferences);
+ function ActivityStreamMessageChannel(options) {
+ this.dispatch = options.dispatch;
+ this.createChannel = sandbox.spy();
+ this.destroyChannel = sandbox.spy();
+ this.middleware = sandbox.spy(s => next => action => next(action));
+ }
+ ({Store, PREF_PREFIX} = injector({"lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel}}));
+ store = new Store();
+ });
+ afterEach(() => {
+ Preferences.clear();
+ globals.restore();
+ });
+ it("should have an .feeds property that is a Map", () => {
+ assert.instanceOf(store.feeds, Map);
+ assert.equal(store.feeds.size, 0, ".feeds.size");
+ });
+ it("should have a redux store at ._store", () => {
+ assert.ok(store._store);
+ assert.property(store, "dispatch");
+ assert.property(store, "getState");
+ });
+ it("should create a ActivityStreamMessageChannel with the right dispatcher", () => {
+ assert.ok(store._messageChannel);
+ assert.equal(store._messageChannel.dispatch, store.dispatch);
+ });
+ it("should connect the ActivityStreamMessageChannel's middleware", () => {
+ store.dispatch({type: "FOO"});
+ assert.calledOnce(store._messageChannel.middleware);
+ });
+ describe("#initFeed", () => {
+ it("should add an instance of the feed to .feeds", () => {
+ class Foo {}
+ Preferences.set(`${PREF_PREFIX}foo`, false);
+ store.init({foo: () => new Foo()});
+ store.initFeed("foo");
+
+ assert.isTrue(store.feeds.has("foo"), "foo is set");
+ assert.instanceOf(store.feeds.get("foo"), Foo);
+ });
+ it("should add a .store property to the feed", () => {
+ class Foo {}
+ store._feedFactories = {foo: () => new Foo()};
+ store.initFeed("foo");
+
+ assert.propertyVal(store.feeds.get("foo"), "store", store);
+ });
+ });
+ describe("#uninitFeed", () => {
+ it("should not throw if no feed with that name exists", () => {
+ assert.doesNotThrow(() => {
+ store.uninitFeed("bar");
+ });
+ });
+ it("should call the feed's uninit function if it is defined", () => {
+ let feed;
+ function createFeed() {
+ feed = {uninit: sinon.spy()};
+ return feed;
+ }
+ store._feedFactories = {foo: createFeed};
+
+ store.initFeed("foo");
+ store.uninitFeed("foo");
+
+ assert.calledOnce(feed.uninit);
+ });
+ it("should remove the feed from .feeds", () => {
+ class Foo {}
+ store._feedFactories = {foo: () => new Foo()};
+
+ store.initFeed("foo");
+ store.uninitFeed("foo");
+
+ assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
+ });
+ });
+ describe("maybeStartFeedAndListenForPrefChanges", () => {
+ beforeEach(() => {
+ sinon.stub(store, "initFeed");
+ sinon.stub(store, "uninitFeed");
+ });
+ it("should set the new pref in Preferences to true, if it was never defined", () => {
+ store.maybeStartFeedAndListenForPrefChanges("foo");
+ assert.isTrue(Preferences.get(`${PREF_PREFIX}foo`));
+ });
+ it("should not override the pref if it was already set", () => {
+ Preferences.set(`${PREF_PREFIX}foo`, false);
+ store.maybeStartFeedAndListenForPrefChanges("foo");
+ assert.isFalse(Preferences.get(`${PREF_PREFIX}foo`));
+ });
+ it("should initialize the feed if the Pref is set to true", () => {
+ Preferences.set(`${PREF_PREFIX}foo`, true);
+ store.maybeStartFeedAndListenForPrefChanges("foo");
+ assert.calledWith(store.initFeed, "foo");
+ });
+ it("should not initialize the feed if the Pref is set to false", () => {
+ Preferences.set(`${PREF_PREFIX}foo`, false);
+ store.maybeStartFeedAndListenForPrefChanges("foo");
+ assert.notCalled(store.initFeed);
+ });
+ it("should observe the pref", () => {
+ store.maybeStartFeedAndListenForPrefChanges("foo");
+ assert.calledWith(Preferences.observe, `${PREF_PREFIX}foo`, store._prefHandlers.get(`${PREF_PREFIX}foo`));
+ });
+ describe("handler", () => {
+ let handler;
+ beforeEach(() => {
+ store.maybeStartFeedAndListenForPrefChanges("foo");
+ handler = store._prefHandlers.get(`${PREF_PREFIX}foo`);
+ });
+ it("should initialize the feed if called with true", () => {
+ handler(true);
+ assert.calledWith(store.initFeed, "foo");
+ });
+ it("should uninitialize the feed if called with false", () => {
+ handler(false);
+ assert.calledWith(store.uninitFeed, "foo");
+ });
+ });
+ });
+ describe("#init", () => {
+ it("should call .maybeStartFeedAndListenForPrefChanges with each key", () => {
+ sinon.stub(store, "maybeStartFeedAndListenForPrefChanges");
+ store.init({foo: () => {}, bar: () => {}});
+ assert.calledWith(store.maybeStartFeedAndListenForPrefChanges, "foo");
+ assert.calledWith(store.maybeStartFeedAndListenForPrefChanges, "bar");
+ });
+ it("should initialize the ActivityStreamMessageChannel channel", () => {
+ store.init();
+ assert.calledOnce(store._messageChannel.createChannel);
+ });
+ });
+ describe("#uninit", () => {
+ it("should clear .feeds, ._prefHandlers, and ._feedFactories", () => {
+ store.init({
+ a: () => ({}),
+ b: () => ({}),
+ c: () => ({})
+ });
+
+ store.uninit();
+
+ assert.equal(store.feeds.size, 0);
+ assert.equal(store._prefHandlers.size, 0);
+ assert.isNull(store._feedFactories);
+ });
+ it("should destroy the ActivityStreamMessageChannel channel", () => {
+ store.uninit();
+ assert.calledOnce(store._messageChannel.destroyChannel);
+ });
+ });
+ describe("#getState", () => {
+ it("should return the redux state", () => {
+ store._store = createStore((prevState = 123) => prevState);
+ const {getState} = store;
+ assert.equal(getState(), 123);
+ });
+ });
+ describe("#dispatch", () => {
+ it("should call .onAction of each feed", () => {
+ const {dispatch} = store;
+ const sub = {onAction: sinon.spy()};
+ const action = {type: "FOO"};
+
+ store.init({sub: () => sub});
+
+ dispatch(action);
+
+ assert.calledWith(sub.onAction, action);
+ });
+ it("should call the reducers", () => {
+ const {dispatch} = store;
+ store._store = createStore(addNumberReducer);
+
+ dispatch({type: "ADD", data: 14});
+
+ assert.equal(store.getState(), 14);
+ });
+ });
+ describe("#subscribe", () => {
+ it("should subscribe to changes to the store", () => {
+ const sub = sinon.spy();
+ const action = {type: "FOO"};
+
+ store.subscribe(sub);
+ store.dispatch(action);
+
+ assert.calledOnce(sub);
+ });
+ });
+});
diff --git a/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js b/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
new file mode 100644
index 000000000000..7e012f9e82c1
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
@@ -0,0 +1,271 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+const {GlobalOverrider, FakePrefs} = require("test/unit/utils");
+const {TelemetrySender} = require("lib/TelemetrySender.jsm");
+
+/**
+ * A reference to the fake preferences object created by the TelemetrySender
+ * constructor so that we can use the API.
+ */
+let fakePrefs;
+const prefInitHook = function() {
+ fakePrefs = this; // eslint-disable-line consistent-this
+};
+const tsArgs = {prefInitHook};
+
+describe("TelemetrySender", () => {
+ let globals;
+ let tSender;
+ let fetchStub;
+ const observerTopics = ["user-action-event", "performance-event",
+ "tab-session-complete", "undesired-event"];
+ const fakeEndpointUrl = "http://127.0.0.1/stuff";
+ const fakePingJSON = JSON.stringify({action: "fake_action", monkey: 1});
+ const fakeFetchHttpErrorResponse = {ok: false, status: 400};
+ const fakeFetchSuccessResponse = {ok: true, status: 200};
+
+ function assertNotificationObserversAdded() {
+ observerTopics.forEach(topic => {
+ assert.calledWithExactly(
+ global.Services.obs.addObserver, tSender, topic, true);
+ });
+ }
+
+ function assertNotificationObserversRemoved() {
+ observerTopics.forEach(topic => {
+ assert.calledWithExactly(
+ global.Services.obs.removeObserver, tSender, topic);
+ });
+ }
+
+ before(() => {
+ globals = new GlobalOverrider();
+
+ fetchStub = globals.sandbox.stub();
+
+ globals.set("Preferences", FakePrefs);
+ globals.set("fetch", fetchStub);
+ });
+
+ beforeEach(() => {
+ });
+
+ afterEach(() => {
+ globals.reset();
+ FakePrefs.prototype.prefs = {};
+ });
+
+ after(() => globals.restore());
+
+ it("should construct the Prefs object with the right branch", () => {
+ globals.sandbox.spy(global, "Preferences");
+
+ tSender = new TelemetrySender(tsArgs);
+
+ assert.calledOnce(global.Preferences);
+ assert.calledWith(global.Preferences,
+ sinon.match.has("branch", "browser.newtabpage.activity-stream"));
+ });
+
+ it("should set the enabled prop to false if the pref is false", () => {
+ FakePrefs.prototype.prefs = {telemetry: false};
+
+ tSender = new TelemetrySender(tsArgs);
+
+ assert.isFalse(tSender.enabled);
+ });
+
+ it("should not add notification observers if the enabled pref is false", () => {
+ FakePrefs.prototype.prefs = {telemetry: false};
+
+ tSender = new TelemetrySender(tsArgs);
+
+ assert.notCalled(global.Services.obs.addObserver);
+ });
+
+ it("should set the enabled prop to true if the pref is true", () => {
+ FakePrefs.prototype.prefs = {telemetry: true};
+
+ tSender = new TelemetrySender(tsArgs);
+
+ assert.isTrue(tSender.enabled);
+ });
+
+ it("should add all notification observers if the enabled pref is true", () => {
+ FakePrefs.prototype.prefs = {telemetry: true};
+
+ tSender = new TelemetrySender(tsArgs);
+
+ assertNotificationObserversAdded();
+ });
+
+ describe("#_sendPing()", () => {
+ beforeEach(() => {
+ FakePrefs.prototype.prefs = {
+ "telemetry": true,
+ "telemetry.ping.endpoint": fakeEndpointUrl
+ };
+ tSender = new TelemetrySender(tsArgs);
+ });
+
+ it("should POST given ping data to telemetry.ping.endpoint pref w/fetch",
+ async () => {
+ fetchStub.resolves(fakeFetchSuccessResponse);
+ await tSender._sendPing(fakePingJSON);
+
+ assert.calledOnce(fetchStub);
+ assert.calledWithExactly(fetchStub, fakeEndpointUrl,
+ {method: "POST", body: fakePingJSON});
+ });
+
+ it("should log HTTP failures using Cu.reportError", async () => {
+ fetchStub.resolves(fakeFetchHttpErrorResponse);
+
+ await tSender._sendPing(fakePingJSON);
+
+ assert.called(Components.utils.reportError);
+ });
+
+ it("should log an error using Cu.reportError if fetch rejects", async () => {
+ fetchStub.rejects("Oh noes!");
+
+ await tSender._sendPing(fakePingJSON);
+
+ assert.called(Components.utils.reportError);
+ });
+
+ it("should log if logging is on && if action is not activity_stream_performance", async () => {
+ FakePrefs.prototype.prefs = {
+ "telemetry": true,
+ "performance.log": true
+ };
+ fetchStub.resolves(fakeFetchSuccessResponse);
+ tSender = new TelemetrySender(tsArgs);
+
+ await tSender._sendPing(fakePingJSON);
+
+ assert.called(console.log); // eslint-disable-line no-console
+ });
+ });
+
+ describe("#observe()", () => {
+ before(() => {
+ globals.sandbox.stub(TelemetrySender.prototype, "_sendPing");
+ });
+
+ observerTopics.forEach(topic => {
+ it(`should call this._sendPing with data for ${topic}`, () => {
+ const fakeSubject = "fakeSubject";
+ tSender = new TelemetrySender(tsArgs);
+
+ tSender.observe(fakeSubject, topic, fakePingJSON);
+
+ assert.calledOnce(TelemetrySender.prototype._sendPing);
+ assert.calledWithExactly(TelemetrySender.prototype._sendPing,
+ fakePingJSON);
+ });
+ });
+
+ it("should not call this._sendPing for 'nonexistent-topic'", () => {
+ const fakeSubject = "fakeSubject";
+ tSender = new TelemetrySender(tsArgs);
+
+ tSender.observe(fakeSubject, "nonexistent-topic", fakePingJSON);
+
+ assert.notCalled(TelemetrySender.prototype._sendPing);
+ });
+ });
+
+ describe("#uninit()", () => {
+ it("should remove the telemetry pref listener", () => {
+ tSender = new TelemetrySender(tsArgs);
+ assert.property(fakePrefs.observers, "telemetry");
+
+ tSender.uninit();
+
+ assert.notProperty(fakePrefs.observers, "telemetry");
+ });
+
+ it("should remove all notification observers if telemetry pref is true", () => {
+ FakePrefs.prototype.prefs = {telemetry: true};
+ tSender = new TelemetrySender(tsArgs);
+
+ tSender.uninit();
+
+ assertNotificationObserversRemoved();
+ });
+
+ it("should not remove notification observers if telemetry pref is false", () => {
+ FakePrefs.prototype.prefs = {telemetry: false};
+ tSender = new TelemetrySender(tsArgs);
+
+ tSender.uninit();
+
+ assert.notCalled(global.Services.obs.removeObserver);
+ });
+
+ it("should call Cu.reportError if this._prefs.ignore throws", () => {
+ globals.sandbox.stub(FakePrefs.prototype, "ignore").throws("Some Error");
+ tSender = new TelemetrySender(tsArgs);
+
+ tSender.uninit();
+
+ assert.called(global.Components.utils.reportError);
+ });
+ });
+
+ describe("Misc pref changes", () => {
+ describe("telemetry changes from true to false", () => {
+ beforeEach(() => {
+ FakePrefs.prototype.prefs = {"telemetry": true};
+ tSender = new TelemetrySender(tsArgs);
+ assert.propertyVal(tSender, "enabled", true);
+ });
+
+ it("should set the enabled property to false", () => {
+ fakePrefs.set("telemetry", false);
+
+ assert.propertyVal(tSender, "enabled", false);
+ });
+
+ it("should remove all notification observers", () => {
+ fakePrefs.set("telemetry", false);
+
+ assertNotificationObserversRemoved();
+ });
+ });
+
+ describe("telemetry changes from false to true", () => {
+ beforeEach(() => {
+ FakePrefs.prototype.prefs = {"telemetry": false};
+ tSender = new TelemetrySender(tsArgs);
+ assert.propertyVal(tSender, "enabled", false);
+ });
+
+ it("should set the enabled property to true", () => {
+ fakePrefs.set("telemetry", true);
+
+ assert.propertyVal(tSender, "enabled", true);
+ });
+
+ it("should add all topic observers", () => {
+ fakePrefs.set("telemetry", true);
+
+ assertNotificationObserversAdded();
+ });
+ });
+
+ describe("performance.log changes from false to true", () => {
+ it("should change this.logging from false to true", () => {
+ FakePrefs.prototype.prefs = {"performance.log": false};
+ tSender = new TelemetrySender(tsArgs);
+ assert.propertyVal(tSender, "logging", false);
+
+ fakePrefs.set("performance.log", true);
+
+ assert.propertyVal(tSender, "logging", true);
+ });
+ });
+ });
+});
diff --git a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
new file mode 100644
index 000000000000..2bd9adb69905
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -0,0 +1,116 @@
+"use strict";
+const {TopSitesFeed, UPDATE_TIME, TOP_SITES_SHOWMORE_LENGTH, DEFAULT_TOP_SITES} = require("lib/TopSitesFeed.jsm");
+const {GlobalOverrider} = require("test/unit/utils");
+const action = {meta: {fromTarget: {}}};
+const {actionTypes: at} = require("common/Actions.jsm");
+const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `site${i}.com`}));
+const FAKE_SCREENSHOT = "data123";
+
+describe("Top Sites Feed", () => {
+ let feed;
+ let globals;
+ let sandbox;
+ let links;
+ let clock;
+ before(() => {
+ globals = new GlobalOverrider();
+ sandbox = globals.sandbox;
+ });
+ beforeEach(() => {
+ globals.set("PlacesProvider", {links: {getLinks: sandbox.spy(() => Promise.resolve(links))}});
+ globals.set("PreviewProvider", {getThumbnail: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))});
+ feed = new TopSitesFeed();
+ feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
+ links = FAKE_LINKS;
+ clock = sinon.useFakeTimers();
+ });
+ afterEach(() => {
+ globals.restore();
+ clock.restore();
+ });
+
+ it("should have default sites with .isDefault = true", () => {
+ DEFAULT_TOP_SITES.forEach(link => assert.propertyVal(link, "isDefault", true));
+ });
+
+ describe("#getLinksWithDefaults", () => {
+ it("should get the links from Places Provider", async () => {
+ const result = await feed.getLinksWithDefaults();
+ assert.deepEqual(result, links);
+ assert.calledOnce(global.PlacesProvider.links.getLinks);
+ });
+ it("should add defaults if there are are not enough links", async () => {
+ links = [{url: "foo.com"}];
+ const result = await feed.getLinksWithDefaults();
+ assert.deepEqual(result, [{url: "foo.com"}, ...DEFAULT_TOP_SITES]);
+ });
+ it("should only add defaults up to TOP_SITES_SHOWMORE_LENGTH", async () => {
+ links = new Array(TOP_SITES_SHOWMORE_LENGTH - 1).fill({url: "foo.com"});
+ const result = await feed.getLinksWithDefaults();
+ assert.lengthOf(result, TOP_SITES_SHOWMORE_LENGTH);
+ assert.deepEqual(result, [...links, DEFAULT_TOP_SITES[0]]);
+ });
+ it("should not throw if PlacesProvider returns null", () => {
+ links = null;
+ assert.doesNotThrow(() => {
+ feed.getLinksWithDefaults(action);
+ });
+ });
+ });
+ describe("#refresh", () => {
+ it("should dispatch an action with the links returned", async () => {
+ sandbox.stub(feed, "getScreenshot");
+ await feed.refresh(action);
+ assert.calledOnce(feed.store.dispatch);
+ assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
+ assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, links);
+ });
+ it("should call .getScreenshot for each link", async () => {
+ sandbox.stub(feed, "getScreenshot");
+ await feed.refresh(action);
+
+ links.forEach(link => assert.calledWith(feed.getScreenshot, link.url));
+ });
+ });
+ describe("getScreenshot", () => {
+ it("should call PreviewProvider.getThumbnail with the right url", async () => {
+ const url = "foo.com";
+ await feed.getScreenshot(url);
+ assert.calledWith(global.PreviewProvider.getThumbnail, url);
+ });
+ });
+ describe("#onAction", () => {
+ it("should call refresh if there are not enough sites on NEW_TAB_LOAD", () => {
+ feed.store.getState = function() { return {TopSites: {rows: []}}; };
+ sinon.stub(feed, "refresh");
+ feed.onAction({type: at.NEW_TAB_LOAD});
+ assert.calledOnce(feed.refresh);
+ });
+ it("should call refresh if there are not sites on NEW_TAB_LOAD, not counting defaults", () => {
+ feed.store.getState = function() { return {TopSites: {rows: [{url: "foo.com"}, ...DEFAULT_TOP_SITES]}}; };
+ sinon.stub(feed, "refresh");
+ feed.onAction({type: at.NEW_TAB_LOAD});
+ assert.calledOnce(feed.refresh);
+ });
+ it("should not call refresh if there are enough sites on NEW_TAB_LOAD", () => {
+ feed.lastUpdated = Date.now();
+ sinon.stub(feed, "refresh");
+ feed.onAction({type: at.NEW_TAB_LOAD});
+ assert.notCalled(feed.refresh);
+ });
+ it("should call refresh if .lastUpdated is too old on NEW_TAB_LOAD", () => {
+ feed.lastUpdated = 0;
+ clock.tick(UPDATE_TIME);
+ sinon.stub(feed, "refresh");
+ feed.onAction({type: at.NEW_TAB_LOAD});
+ assert.calledOnce(feed.refresh);
+ });
+ it("should not call refresh if .lastUpdated is less than update time on NEW_TAB_LOAD", () => {
+ feed.lastUpdated = 0;
+ clock.tick(UPDATE_TIME - 1);
+ sinon.stub(feed, "refresh");
+ feed.onAction({type: at.NEW_TAB_LOAD});
+ assert.notCalled(feed.refresh);
+ });
+ });
+});
diff --git a/browser/extensions/activity-stream/test/unit/lib/init-store.test.js b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
new file mode 100644
index 000000000000..190d632d6289
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
@@ -0,0 +1,43 @@
+const initStore = require("content-src/lib/init-store");
+const {GlobalOverrider, addNumberReducer} = require("test/unit/utils");
+const {actionCreators: ac} = require("common/Actions.jsm");
+
+describe("initStore", () => {
+ let globals;
+ let store;
+ before(() => {
+ globals = new GlobalOverrider();
+ globals.set("sendAsyncMessage", globals.sandbox.spy());
+ globals.set("addMessageListener", globals.sandbox.spy());
+ });
+ beforeEach(() => {
+ store = initStore({number: addNumberReducer});
+ });
+ afterEach(() => globals.reset());
+ after(() => globals.restore());
+ it("should create a store with the provided reducers", () => {
+ assert.ok(store);
+ assert.property(store.getState(), "number");
+ });
+ it("should add a listener for incoming actions", () => {
+ assert.calledWith(global.addMessageListener, initStore.INCOMING_MESSAGE_NAME);
+ const callback = global.addMessageListener.firstCall.args[1];
+ globals.sandbox.spy(store, "dispatch");
+ const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: "FOO"}};
+ callback(message);
+ assert.calledWith(store.dispatch, message.data);
+ });
+ it("should replace the state if a MERGE_STORE_ACTION is dispatched", () => {
+ store.dispatch({type: initStore.MERGE_STORE_ACTION, data: {number: 42}});
+ assert.deepEqual(store.getState(), {number: 42});
+ });
+ it("should send out SendToMain ations", () => {
+ const action = ac.SendToMain({type: "FOO"});
+ store.dispatch(action);
+ assert.calledWith(global.sendAsyncMessage, initStore.OUTGOING_MESSAGE_NAME, action);
+ });
+ it("should not send out other types of ations", () => {
+ store.dispatch({type: "FOO"});
+ assert.notCalled(global.sendAsyncMessage);
+ });
+});
diff --git a/browser/extensions/activity-stream/test/unit/unit-entry.js b/browser/extensions/activity-stream/test/unit/unit-entry.js
new file mode 100644
index 000000000000..832c3e6d116f
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/unit-entry.js
@@ -0,0 +1,38 @@
+const {GlobalOverrider} = require("test/unit/utils");
+
+const req = require.context(".", true, /\.test\.js$/);
+const files = req.keys();
+
+// This exposes sinon assertions to chai.assert
+sinon.assert.expose(assert, {prefix: ""});
+
+let overrider = new GlobalOverrider();
+overrider.set({
+ Components: {
+ interfaces: {},
+ utils: {
+ import: overrider.sandbox.spy(),
+ importGlobalProperties: overrider.sandbox.spy(),
+ reportError: overrider.sandbox.spy()
+ }
+ },
+ XPCOMUtils: {
+ defineLazyModuleGetter: overrider.sandbox.spy(),
+ defineLazyServiceGetter: overrider.sandbox.spy(),
+ generateQI: overrider.sandbox.stub().returns(() => {})
+ },
+ console: {log: overrider.sandbox.spy()},
+ dump: overrider.sandbox.spy(),
+ Services: {
+ obs: {
+ addObserver: overrider.sandbox.spy(),
+ removeObserver: overrider.sandbox.spy()
+ }
+ }
+});
+
+describe("activity-stream", () => {
+ afterEach(() => overrider.reset());
+ after(() => overrider.restore());
+ files.forEach(file => req(file));
+});
diff --git a/browser/extensions/activity-stream/test/unit/utils.js b/browser/extensions/activity-stream/test/unit/utils.js
new file mode 100644
index 000000000000..dd5e4a3a4cc1
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/utils.js
@@ -0,0 +1,122 @@
+/**
+ * GlobalOverrider - Utility that allows you to override properties on the global object.
+ * See unit-entry.js for example usage.
+ */
+class GlobalOverrider {
+ constructor() {
+ this.originalGlobals = new Map();
+ this.sandbox = sinon.sandbox.create();
+ }
+
+ /**
+ * _override - Internal method to override properties on the global object.
+ * The first time a given key is overridden, we cache the original
+ * value in this.originalGlobals so that later it can be restored.
+ *
+ * @param {string} key The identifier of the property
+ * @param {any} value The value to which the property should be reassigned
+ */
+ _override(key, value) {
+ if (key === "Components") {
+ // Components can be reassigned, but it will subsequently throw a deprecation
+ // error in Firefox which will stop execution. Adding the assignment statement
+ // to a try/catch block will prevent this from happening.
+ try {
+ global[key] = value;
+ } catch (e) {} // eslint-disable-line no-empty
+ return;
+ }
+ if (!this.originalGlobals.has(key)) {
+ this.originalGlobals.set(key, global[key]);
+ }
+ global[key] = value;
+ }
+
+ /**
+ * set - Override a given property, or all properties on an object
+ *
+ * @param {string|object} key If a string, the identifier of the property
+ * If an object, a number of properties and values to which they should be reassigned.
+ * @param {any} value The value to which the property should be reassigned
+ * @return {type} description
+ */
+ set(key, value) {
+ if (!value && typeof key === "object") {
+ const overrides = key;
+ Object.keys(overrides).forEach(k => this._override(k, overrides[k]));
+ } else {
+ this._override(key, value);
+ }
+ }
+
+ /**
+ * reset - Reset the global sandbox, so all state on spies, stubs etc. is cleared.
+ * You probably want to call this after each test.
+ */
+ reset() {
+ this.sandbox.reset();
+ }
+
+ /**
+ * restore - Restore the global sandbox and reset all overriden properties to
+ * their original values. You should call this after all tests have completed.
+ */
+ restore() {
+ this.sandbox.restore();
+ this.originalGlobals.forEach((value, key) => {
+ global[key] = value;
+ });
+ }
+}
+
+/**
+ * Very simple fake for the most basic semantics of Preferences.jsm. Lots of
+ * things aren't yet supported. Feel free to add them in.
+ *
+ * @param {Object} args - optional arguments
+ * @param {Function} args.initHook - if present, will be called back
+ * inside the constructor. Typically used from tests
+ * to save off a pointer to the created instance so that
+ * stubs and spies can be inspected by the test code.
+ */
+function FakePrefs(args) {
+ if (args) {
+ if ("initHook" in args) {
+ args.initHook.call(this);
+ }
+ }
+}
+FakePrefs.prototype = {
+ observers: {},
+ observe(prefName, callback) {
+ this.observers[prefName] = callback;
+ },
+ ignore(prefName, callback) {
+ if (prefName in this.observers) {
+ delete this.observers[prefName];
+ }
+ },
+
+ prefs: {},
+ get(prefName) { return this.prefs[prefName]; },
+ set(prefName, value) {
+ this.prefs[prefName] = value;
+
+ if (prefName in this.observers) {
+ this.observers[prefName](value);
+ }
+ }
+};
+
+/**
+ * addNumberReducer - a simple dummy reducer for testing that adds a number
+ */
+function addNumberReducer(prevState = 0, action) {
+ return action.type === "ADD" ? prevState + action.data : prevState;
+}
+
+module.exports = {
+ FakePrefs,
+ GlobalOverrider,
+ addNumberReducer
+};
diff --git a/browser/extensions/activity-stream/vendor/redux.js b/browser/extensions/activity-stream/vendor/redux.js
new file mode 100644
index 000000000000..b83c31d3a25b
--- /dev/null
+++ b/browser/extensions/activity-stream/vendor/redux.js
@@ -0,0 +1,948 @@
+/**
+ * Redux v.3.6.0
+ */
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define([], factory);
+ else if(typeof exports === 'object')
+ exports["Redux"] = factory();
+ else
+ root["Redux"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.compose = exports.applyMiddleware = exports.bindActionCreators = exports.combineReducers = exports.createStore = undefined;
+
+ var _createStore = __webpack_require__(2);
+
+ var _createStore2 = _interopRequireDefault(_createStore);
+
+ var _combineReducers = __webpack_require__(7);
+
+ var _combineReducers2 = _interopRequireDefault(_combineReducers);
+
+ var _bindActionCreators = __webpack_require__(6);
+
+ var _bindActionCreators2 = _interopRequireDefault(_bindActionCreators);
+
+ var _applyMiddleware = __webpack_require__(5);
+
+ var _applyMiddleware2 = _interopRequireDefault(_applyMiddleware);
+
+ var _compose = __webpack_require__(1);
+
+ var _compose2 = _interopRequireDefault(_compose);
+
+ var _warning = __webpack_require__(3);
+
+ var _warning2 = _interopRequireDefault(_warning);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ /*
+ * This is a dummy function to check if the function name has been altered by minification.
+ * If the function has been minified and NODE_ENV !== 'production', warn the user.
+ */
+ function isCrushed() {}
+
+ if (("development") !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed') {
+ (0, _warning2['default'])('You are currently using minified code outside of NODE_ENV === \'production\'. ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 'to ensure you have the correct code for your production build.');
+ }
+
+ exports.createStore = _createStore2['default'];
+ exports.combineReducers = _combineReducers2['default'];
+ exports.bindActionCreators = _bindActionCreators2['default'];
+ exports.applyMiddleware = _applyMiddleware2['default'];
+ exports.compose = _compose2['default'];
+
+/***/ },
+/* 1 */
+/***/ function(module, exports) {
+
+ "use strict";
+
+ exports.__esModule = true;
+ exports["default"] = compose;
+ /**
+ * Composes single-argument functions from right to left. The rightmost
+ * function can take multiple arguments as it provides the signature for
+ * the resulting composite function.
+ *
+ * @param {...Function} funcs The functions to compose.
+ * @returns {Function} A function obtained by composing the argument functions
+ * from right to left. For example, compose(f, g, h) is identical to doing
+ * (...args) => f(g(h(...args))).
+ */
+
+ function compose() {
+ for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
+ funcs[_key] = arguments[_key];
+ }
+
+ if (funcs.length === 0) {
+ return function (arg) {
+ return arg;
+ };
+ }
+
+ if (funcs.length === 1) {
+ return funcs[0];
+ }
+
+ var last = funcs[funcs.length - 1];
+ var rest = funcs.slice(0, -1);
+ return function () {
+ return rest.reduceRight(function (composed, f) {
+ return f(composed);
+ }, last.apply(undefined, arguments));
+ };
+ }
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports.ActionTypes = undefined;
+ exports['default'] = createStore;
+
+ var _isPlainObject = __webpack_require__(4);
+
+ var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+ var _symbolObservable = __webpack_require__(12);
+
+ var _symbolObservable2 = _interopRequireDefault(_symbolObservable);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ /**
+ * These are private action types reserved by Redux.
+ * For any unknown actions, you must return the current state.
+ * If the current state is undefined, you must return the initial state.
+ * Do not reference these action types directly in your code.
+ */
+ var ActionTypes = exports.ActionTypes = {
+ INIT: '@@redux/INIT'
+ };
+
+ /**
+ * Creates a Redux store that holds the state tree.
+ * The only way to change the data in the store is to call `dispatch()` on it.
+ *
+ * There should only be a single store in your app. To specify how different
+ * parts of the state tree respond to actions, you may combine several reducers
+ * into a single reducer function by using `combineReducers`.
+ *
+ * @param {Function} reducer A function that returns the next state tree, given
+ * the current state tree and the action to handle.
+ *
+ * @param {any} [preloadedState] The initial state. You may optionally specify it
+ * to hydrate the state from the server in universal apps, or to restore a
+ * previously serialized user session.
+ * If you use `combineReducers` to produce the root reducer function, this must be
+ * an object with the same shape as `combineReducers` keys.
+ *
+ * @param {Function} enhancer The store enhancer. You may optionally specify it
+ * to enhance the store with third-party capabilities such as middleware,
+ * time travel, persistence, etc. The only store enhancer that ships with Redux
+ * is `applyMiddleware()`.
+ *
+ * @returns {Store} A Redux store that lets you read the state, dispatch actions
+ * and subscribe to changes.
+ */
+ function createStore(reducer, preloadedState, enhancer) {
+ var _ref2;
+
+ if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
+ enhancer = preloadedState;
+ preloadedState = undefined;
+ }
+
+ if (typeof enhancer !== 'undefined') {
+ if (typeof enhancer !== 'function') {
+ throw new Error('Expected the enhancer to be a function.');
+ }
+
+ return enhancer(createStore)(reducer, preloadedState);
+ }
+
+ if (typeof reducer !== 'function') {
+ throw new Error('Expected the reducer to be a function.');
+ }
+
+ var currentReducer = reducer;
+ var currentState = preloadedState;
+ var currentListeners = [];
+ var nextListeners = currentListeners;
+ var isDispatching = false;
+
+ function ensureCanMutateNextListeners() {
+ if (nextListeners === currentListeners) {
+ nextListeners = currentListeners.slice();
+ }
+ }
+
+ /**
+ * Reads the state tree managed by the store.
+ *
+ * @returns {any} The current state tree of your application.
+ */
+ function getState() {
+ return currentState;
+ }
+
+ /**
+ * Adds a change listener. It will be called any time an action is dispatched,
+ * and some part of the state tree may potentially have changed. You may then
+ * call `getState()` to read the current state tree inside the callback.
+ *
+ * You may call `dispatch()` from a change listener, with the following
+ * caveats:
+ *
+ * 1. The subscriptions are snapshotted just before every `dispatch()` call.
+ * If you subscribe or unsubscribe while the listeners are being invoked, this
+ * will not have any effect on the `dispatch()` that is currently in progress.
+ * However, the next `dispatch()` call, whether nested or not, will use a more
+ * recent snapshot of the subscription list.
+ *
+ * 2. The listener should not expect to see all state changes, as the state
+ * might have been updated multiple times during a nested `dispatch()` before
+ * the listener is called. It is, however, guaranteed that all subscribers
+ * registered before the `dispatch()` started will be called with the latest
+ * state by the time it exits.
+ *
+ * @param {Function} listener A callback to be invoked on every dispatch.
+ * @returns {Function} A function to remove this change listener.
+ */
+ function subscribe(listener) {
+ if (typeof listener !== 'function') {
+ throw new Error('Expected listener to be a function.');
+ }
+
+ var isSubscribed = true;
+
+ ensureCanMutateNextListeners();
+ nextListeners.push(listener);
+
+ return function unsubscribe() {
+ if (!isSubscribed) {
+ return;
+ }
+
+ isSubscribed = false;
+
+ ensureCanMutateNextListeners();
+ var index = nextListeners.indexOf(listener);
+ nextListeners.splice(index, 1);
+ };
+ }
+
+ /**
+ * Dispatches an action. It is the only way to trigger a state change.
+ *
+ * The `reducer` function, used to create the store, will be called with the
+ * current state tree and the given `action`. Its return value will
+ * be considered the **next** state of the tree, and the change listeners
+ * will be notified.
+ *
+ * The base implementation only supports plain object actions. If you want to
+ * dispatch a Promise, an Observable, a thunk, or something else, you need to
+ * wrap your store creating function into the corresponding middleware. For
+ * example, see the documentation for the `redux-thunk` package. Even the
+ * middleware will eventually dispatch plain object actions using this method.
+ *
+ * @param {Object} action A plain object representing “what changedâ€. It is
+ * a good idea to keep actions serializable so you can record and replay user
+ * sessions, or use the time travelling `redux-devtools`. An action must have
+ * a `type` property which may not be `undefined`. It is a good idea to use
+ * string constants for action types.
+ *
+ * @returns {Object} For convenience, the same action object you dispatched.
+ *
+ * Note that, if you use a custom middleware, it may wrap `dispatch()` to
+ * return something else (for example, a Promise you can await).
+ */
+ function dispatch(action) {
+ if (!(0, _isPlainObject2['default'])(action)) {
+ throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
+ }
+
+ if (typeof action.type === 'undefined') {
+ throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
+ }
+
+ if (isDispatching) {
+ throw new Error('Reducers may not dispatch actions.');
+ }
+
+ try {
+ isDispatching = true;
+ currentState = currentReducer(currentState, action);
+ } finally {
+ isDispatching = false;
+ }
+
+ var listeners = currentListeners = nextListeners;
+ for (var i = 0; i < listeners.length; i++) {
+ listeners[i]();
+ }
+
+ return action;
+ }
+
+ /**
+ * Replaces the reducer currently used by the store to calculate the state.
+ *
+ * You might need this if your app implements code splitting and you want to
+ * load some of the reducers dynamically. You might also need this if you
+ * implement a hot reloading mechanism for Redux.
+ *
+ * @param {Function} nextReducer The reducer for the store to use instead.
+ * @returns {void}
+ */
+ function replaceReducer(nextReducer) {
+ if (typeof nextReducer !== 'function') {
+ throw new Error('Expected the nextReducer to be a function.');
+ }
+
+ currentReducer = nextReducer;
+ dispatch({ type: ActionTypes.INIT });
+ }
+
+ /**
+ * Interoperability point for observable/reactive libraries.
+ * @returns {observable} A minimal observable of state changes.
+ * For more information, see the observable proposal:
+ * https://github.com/zenparsing/es-observable
+ */
+ function observable() {
+ var _ref;
+
+ var outerSubscribe = subscribe;
+ return _ref = {
+ /**
+ * The minimal observable subscription method.
+ * @param {Object} observer Any object that can be used as an observer.
+ * The observer object should have a `next` method.
+ * @returns {subscription} An object with an `unsubscribe` method that can
+ * be used to unsubscribe the observable from the store, and prevent further
+ * emission of values from the observable.
+ */
+ subscribe: function subscribe(observer) {
+ if (typeof observer !== 'object') {
+ throw new TypeError('Expected the observer to be an object.');
+ }
+
+ function observeState() {
+ if (observer.next) {
+ observer.next(getState());
+ }
+ }
+
+ observeState();
+ var unsubscribe = outerSubscribe(observeState);
+ return { unsubscribe: unsubscribe };
+ }
+ }, _ref[_symbolObservable2['default']] = function () {
+ return this;
+ }, _ref;
+ }
+
+ // When a store is created, an "INIT" action is dispatched so that every
+ // reducer returns their initial state. This effectively populates
+ // the initial state tree.
+ dispatch({ type: ActionTypes.INIT });
+
+ return _ref2 = {
+ dispatch: dispatch,
+ subscribe: subscribe,
+ getState: getState,
+ replaceReducer: replaceReducer
+ }, _ref2[_symbolObservable2['default']] = observable, _ref2;
+ }
+
+/***/ },
+/* 3 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = warning;
+ /**
+ * Prints a warning in the console if it exists.
+ *
+ * @param {String} message The warning message.
+ * @returns {void}
+ */
+ function warning(message) {
+ /* eslint-disable no-console */
+ if (typeof console !== 'undefined' && typeof console.error === 'function') {
+ console.error(message);
+ }
+ /* eslint-enable no-console */
+ try {
+ // This error was thrown as a convenience so that if you enable
+ // "break on all exceptions" in your console,
+ // it would pause the execution at this line.
+ throw new Error(message);
+ /* eslint-disable no-empty */
+ } catch (e) {}
+ /* eslint-enable no-empty */
+ }
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var getPrototype = __webpack_require__(8),
+ isHostObject = __webpack_require__(9),
+ isObjectLike = __webpack_require__(11);
+
+ /** `Object#toString` result references. */
+ var objectTag = '[object Object]';
+
+ /** Used for built-in method references. */
+ var funcProto = Function.prototype,
+ objectProto = Object.prototype;
+
+ /** Used to resolve the decompiled source of functions. */
+ var funcToString = funcProto.toString;
+
+ /** Used to check objects for own properties. */
+ var hasOwnProperty = objectProto.hasOwnProperty;
+
+ /** Used to infer the `Object` constructor. */
+ var objectCtorString = funcToString.call(Object);
+
+ /**
+ * Used to resolve the
+ * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+ var objectToString = objectProto.toString;
+
+ /**
+ * Checks if `value` is a plain object, that is, an object created by the
+ * `Object` constructor or one with a `[[Prototype]]` of `null`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.8.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * }
+ *
+ * _.isPlainObject(new Foo);
+ * // => false
+ *
+ * _.isPlainObject([1, 2, 3]);
+ * // => false
+ *
+ * _.isPlainObject({ 'x': 0, 'y': 0 });
+ * // => true
+ *
+ * _.isPlainObject(Object.create(null));
+ * // => true
+ */
+ function isPlainObject(value) {
+ if (!isObjectLike(value) ||
+ objectToString.call(value) != objectTag || isHostObject(value)) {
+ return false;
+ }
+ var proto = getPrototype(value);
+ if (proto === null) {
+ return true;
+ }
+ var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
+ return (typeof Ctor == 'function' &&
+ Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
+ }
+
+ module.exports = isPlainObject;
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ exports['default'] = applyMiddleware;
+
+ var _compose = __webpack_require__(1);
+
+ var _compose2 = _interopRequireDefault(_compose);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ /**
+ * Creates a store enhancer that applies middleware to the dispatch method
+ * of the Redux store. This is handy for a variety of tasks, such as expressing
+ * asynchronous actions in a concise manner, or logging every action payload.
+ *
+ * See `redux-thunk` package as an example of the Redux middleware.
+ *
+ * Because middleware is potentially asynchronous, this should be the first
+ * store enhancer in the composition chain.
+ *
+ * Note that each middleware will be given the `dispatch` and `getState` functions
+ * as named arguments.
+ *
+ * @param {...Function} middlewares The middleware chain to be applied.
+ * @returns {Function} A store enhancer applying the middleware.
+ */
+ function applyMiddleware() {
+ for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
+ middlewares[_key] = arguments[_key];
+ }
+
+ return function (createStore) {
+ return function (reducer, preloadedState, enhancer) {
+ var store = createStore(reducer, preloadedState, enhancer);
+ var _dispatch = store.dispatch;
+ var chain = [];
+
+ var middlewareAPI = {
+ getState: store.getState,
+ dispatch: function dispatch(action) {
+ return _dispatch(action);
+ }
+ };
+ chain = middlewares.map(function (middleware) {
+ return middleware(middlewareAPI);
+ });
+ _dispatch = _compose2['default'].apply(undefined, chain)(store.dispatch);
+
+ return _extends({}, store, {
+ dispatch: _dispatch
+ });
+ };
+ };
+ }
+
+/***/ },
+/* 6 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = bindActionCreators;
+ function bindActionCreator(actionCreator, dispatch) {
+ return function () {
+ return dispatch(actionCreator.apply(undefined, arguments));
+ };
+ }
+
+ /**
+ * Turns an object whose values are action creators, into an object with the
+ * same keys, but with every function wrapped into a `dispatch` call so they
+ * may be invoked directly. This is just a convenience method, as you can call
+ * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
+ *
+ * For convenience, you can also pass a single function as the first argument,
+ * and get a function in return.
+ *
+ * @param {Function|Object} actionCreators An object whose values are action
+ * creator functions. One handy way to obtain it is to use ES6 `import * as`
+ * syntax. You may also pass a single function.
+ *
+ * @param {Function} dispatch The `dispatch` function available on your Redux
+ * store.
+ *
+ * @returns {Function|Object} The object mimicking the original object, but with
+ * every action creator wrapped into the `dispatch` call. If you passed a
+ * function as `actionCreators`, the return value will also be a single
+ * function.
+ */
+ function bindActionCreators(actionCreators, dispatch) {
+ if (typeof actionCreators === 'function') {
+ return bindActionCreator(actionCreators, dispatch);
+ }
+
+ if (typeof actionCreators !== 'object' || actionCreators === null) {
+ throw new Error('bindActionCreators expected an object or a function, instead received ' + (actionCreators === null ? 'null' : typeof actionCreators) + '. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');
+ }
+
+ var keys = Object.keys(actionCreators);
+ var boundActionCreators = {};
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ var actionCreator = actionCreators[key];
+ if (typeof actionCreator === 'function') {
+ boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
+ }
+ }
+ return boundActionCreators;
+ }
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ exports.__esModule = true;
+ exports['default'] = combineReducers;
+
+ var _createStore = __webpack_require__(2);
+
+ var _isPlainObject = __webpack_require__(4);
+
+ var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+ var _warning = __webpack_require__(3);
+
+ var _warning2 = _interopRequireDefault(_warning);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ function getUndefinedStateErrorMessage(key, action) {
+ var actionType = action && action.type;
+ var actionName = actionType && '"' + actionType.toString() + '"' || 'an action';
+
+ return 'Given action ' + actionName + ', reducer "' + key + '" returned undefined. ' + 'To ignore an action, you must explicitly return the previous state.';
+ }
+
+ function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) {
+ var reducerKeys = Object.keys(reducers);
+ var argumentName = action && action.type === _createStore.ActionTypes.INIT ? 'preloadedState argument passed to createStore' : 'previous state received by the reducer';
+
+ if (reducerKeys.length === 0) {
+ return 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.';
+ }
+
+ if (!(0, _isPlainObject2['default'])(inputState)) {
+ return 'The ' + argumentName + ' has unexpected type of "' + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + '". Expected argument to be an object with the following ' + ('keys: "' + reducerKeys.join('", "') + '"');
+ }
+
+ var unexpectedKeys = Object.keys(inputState).filter(function (key) {
+ return !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key];
+ });
+
+ unexpectedKeys.forEach(function (key) {
+ unexpectedKeyCache[key] = true;
+ });
+
+ if (unexpectedKeys.length > 0) {
+ return 'Unexpected ' + (unexpectedKeys.length > 1 ? 'keys' : 'key') + ' ' + ('"' + unexpectedKeys.join('", "') + '" found in ' + argumentName + '. ') + 'Expected to find one of the known reducer keys instead: ' + ('"' + reducerKeys.join('", "') + '". Unexpected keys will be ignored.');
+ }
+ }
+
+ function assertReducerSanity(reducers) {
+ Object.keys(reducers).forEach(function (key) {
+ var reducer = reducers[key];
+ var initialState = reducer(undefined, { type: _createStore.ActionTypes.INIT });
+
+ if (typeof initialState === 'undefined') {
+ throw new Error('Reducer "' + key + '" returned undefined during initialization. ' + 'If the state passed to the reducer is undefined, you must ' + 'explicitly return the initial state. The initial state may ' + 'not be undefined.');
+ }
+
+ var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');
+ if (typeof reducer(undefined, { type: type }) === 'undefined') {
+ throw new Error('Reducer "' + key + '" returned undefined when probed with a random type. ' + ('Don\'t try to handle ' + _createStore.ActionTypes.INIT + ' or other actions in "redux/*" ') + 'namespace. They are considered private. Instead, you must return the ' + 'current state for any unknown actions, unless it is undefined, ' + 'in which case you must return the initial state, regardless of the ' + 'action type. The initial state may not be undefined.');
+ }
+ });
+ }
+
+ /**
+ * Turns an object whose values are different reducer functions, into a single
+ * reducer function. It will call every child reducer, and gather their results
+ * into a single state object, whose keys correspond to the keys of the passed
+ * reducer functions.
+ *
+ * @param {Object} reducers An object whose values correspond to different
+ * reducer functions that need to be combined into one. One handy way to obtain
+ * it is to use ES6 `import * as reducers` syntax. The reducers may never return
+ * undefined for any action. Instead, they should return their initial state
+ * if the state passed to them was undefined, and the current state for any
+ * unrecognized action.
+ *
+ * @returns {Function} A reducer function that invokes every reducer inside the
+ * passed object, and builds a state object with the same shape.
+ */
+ function combineReducers(reducers) {
+ var reducerKeys = Object.keys(reducers);
+ var finalReducers = {};
+ for (var i = 0; i < reducerKeys.length; i++) {
+ var key = reducerKeys[i];
+
+ if (true) {
+ if (typeof reducers[key] === 'undefined') {
+ (0, _warning2['default'])('No reducer provided for key "' + key + '"');
+ }
+ }
+
+ if (typeof reducers[key] === 'function') {
+ finalReducers[key] = reducers[key];
+ }
+ }
+ var finalReducerKeys = Object.keys(finalReducers);
+
+ if (true) {
+ var unexpectedKeyCache = {};
+ }
+
+ var sanityError;
+ try {
+ assertReducerSanity(finalReducers);
+ } catch (e) {
+ sanityError = e;
+ }
+
+ return function combination() {
+ var state = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+ var action = arguments[1];
+
+ if (sanityError) {
+ throw sanityError;
+ }
+
+ if (true) {
+ var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);
+ if (warningMessage) {
+ (0, _warning2['default'])(warningMessage);
+ }
+ }
+
+ var hasChanged = false;
+ var nextState = {};
+ for (var i = 0; i < finalReducerKeys.length; i++) {
+ var key = finalReducerKeys[i];
+ var reducer = finalReducers[key];
+ var previousStateForKey = state[key];
+ var nextStateForKey = reducer(previousStateForKey, action);
+ if (typeof nextStateForKey === 'undefined') {
+ var errorMessage = getUndefinedStateErrorMessage(key, action);
+ throw new Error(errorMessage);
+ }
+ nextState[key] = nextStateForKey;
+ hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
+ }
+ return hasChanged ? nextState : state;
+ };
+ }
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var overArg = __webpack_require__(10);
+
+ /** Built-in value references. */
+ var getPrototype = overArg(Object.getPrototypeOf, Object);
+
+ module.exports = getPrototype;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if `value` is a host object in IE < 9.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a host object, else `false`.
+ */
+ function isHostObject(value) {
+ // Many host objects are `Object` objects that can coerce to strings
+ // despite having improperly defined `toString` methods.
+ var result = false;
+ if (value != null && typeof value.toString != 'function') {
+ try {
+ result = !!(value + '');
+ } catch (e) {}
+ }
+ return result;
+ }
+
+ module.exports = isHostObject;
+
+
+/***/ },
+/* 10 */
+/***/ function(module, exports) {
+
+ /**
+ * Creates a unary function that invokes `func` with its argument transformed.
+ *
+ * @private
+ * @param {Function} func The function to wrap.
+ * @param {Function} transform The argument transform.
+ * @returns {Function} Returns the new function.
+ */
+ function overArg(func, transform) {
+ return function(arg) {
+ return func(transform(arg));
+ };
+ }
+
+ module.exports = overArg;
+
+
+/***/ },
+/* 11 */
+/***/ function(module, exports) {
+
+ /**
+ * Checks if `value` is object-like. A value is object-like if it's not `null`
+ * and has a `typeof` result of "object".
+ *
+ * @static
+ * @memberOf _
+ * @since 4.0.0
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+ * @example
+ *
+ * _.isObjectLike({});
+ * // => true
+ *
+ * _.isObjectLike([1, 2, 3]);
+ * // => true
+ *
+ * _.isObjectLike(_.noop);
+ * // => false
+ *
+ * _.isObjectLike(null);
+ * // => false
+ */
+ function isObjectLike(value) {
+ return !!value && typeof value == 'object';
+ }
+
+ module.exports = isObjectLike;
+
+
+/***/ },
+/* 12 */
+/***/ function(module, exports, __webpack_require__) {
+
+ module.exports = __webpack_require__(13);
+
+
+/***/ },
+/* 13 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {'use strict';
+
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+
+ var _ponyfill = __webpack_require__(14);
+
+ var _ponyfill2 = _interopRequireDefault(_ponyfill);
+
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+ var root = undefined; /* global window */
+
+ if (typeof global !== 'undefined') {
+ root = global;
+ } else if (typeof window !== 'undefined') {
+ root = window;
+ }
+
+ var result = (0, _ponyfill2['default'])(root);
+ exports['default'] = result;
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 14 */
+/***/ function(module, exports) {
+
+ 'use strict';
+
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+ exports['default'] = symbolObservablePonyfill;
+ function symbolObservablePonyfill(root) {
+ var result;
+ var _Symbol = root.Symbol;
+
+ if (typeof _Symbol === 'function') {
+ if (_Symbol.observable) {
+ result = _Symbol.observable;
+ } else {
+ result = _Symbol('observable');
+ _Symbol.observable = result;
+ }
+ } else {
+ result = '@@observable';
+ }
+
+ return result;
+ };
+
+/***/ }
+/******/ ])
+});
+;
diff --git a/browser/extensions/e10srollout/bootstrap.js b/browser/extensions/e10srollout/bootstrap.js
index df6b0c41c5b7..876fd33c4d58 100644
--- a/browser/extensions/e10srollout/bootstrap.js
+++ b/browser/extensions/e10srollout/bootstrap.js
@@ -11,13 +11,19 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/UpdateUtils.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
- // The amount of people to be part of e10s
+// The amount of people to be part of e10s
const TEST_THRESHOLD = {
"beta": 0.9, // 90%
"release": 1.0, // 100%
"esr": 1.0, // 100%
};
+// If a user qualifies for the e10s-multi experiement, this is how many
+// content processes to use.
+const MULTI_BUCKETS = {
+ "beta": { 1: .5, 4: 1, },
+};
+
const ADDON_ROLLOUT_POLICY = {
"beta": "50allmpc",
"release": "50allmpc",
@@ -120,14 +126,14 @@ function defineCohort() {
cohortPrefix = `addons-set${addonPolicy}-`;
}
- let inMultiExperiment = false;
+ let eligibleForMulti = false;
if (userOptedOut.e10s || userOptedOut.multi) {
// If we detected that the user opted out either for multi or e10s, then
// the proper prefs must already be set.
setCohort("optedOut");
} else if (userOptedIn.e10s) {
setCohort("optedIn");
- inMultiExperiment = true;
+ eligibleForMulti = true;
} else if (temporaryDisqualification != "") {
// Users who are disqualified by the backend (from multiprocessBlockPolicy)
// can be put into either the test or control groups, because e10s will
@@ -147,11 +153,11 @@ function defineCohort() {
// qualification which overrides the user sample value when non-empty.
setCohort(`temp-qualified-${temporaryQualification}`);
Preferences.set(PREF_TOGGLE_E10S, true);
- inMultiExperiment = true;
+ eligibleForMulti = true;
} else if (testGroup) {
setCohort(`${cohortPrefix}test`);
Preferences.set(PREF_TOGGLE_E10S, true);
- inMultiExperiment = true;
+ eligibleForMulti = true;
} else {
setCohort(`${cohortPrefix}control`);
Preferences.reset(PREF_TOGGLE_E10S);
@@ -159,34 +165,37 @@ function defineCohort() {
}
// Now determine if this user should be in the e10s-multi experiment.
- // - We only run the experiment on the beta channel.
+ // - We only run the experiment on channels defined in MULTI_BUCKETS.
// - We decided above whether this user qualifies for the experiment.
// - If the user already opted into multi, then their prefs are already set
// correctly, we're done.
// - If the user has addons that disqualify them for multi, leave them with
// the default number of content processes (1 on beta) but still in the
// test cohort.
- if (updateChannel !== "beta" ||
- !inMultiExperiment ||
+ if (!(updateChannel in MULTI_BUCKETS) ||
+ !eligibleForMulti ||
userOptedIn.multi ||
+ disqualified ||
getAddonsDisqualifyForMulti()) {
Preferences.reset(PREF_E10S_PROCESSCOUNT + ".web");
return;
}
+ // If we got here with a cohortPrefix, it must be "addons-set50allmpc-",
+ // and we know because of getAddonsDisqualifyForMulti that the addons that
+ // are installed must be web extensions.
+ if (cohortPrefix) {
+ cohortPrefix = "webextensions-";
+ }
+
// The user is in the multi experiment!
// Decide how many content processes to use for this user.
- let BUCKETS = {
- 1: .25,
- 2: .5,
- 4: .75,
- 8: 1
- };
+ let buckets = MULTI_BUCKETS[updateChannel];
let multiUserSample = getUserSample(true);
- for (let sampleName of Object.getOwnPropertyNames(BUCKETS)) {
- if (multiUserSample < BUCKETS[sampleName]) {
- setCohort(`multiBucket${sampleName}`);
+ for (let sampleName of Object.getOwnPropertyNames(buckets)) {
+ if (multiUserSample < buckets[sampleName]) {
+ setCohort(`${cohortPrefix}multiBucket${sampleName}`);
Preferences.set(PREF_E10S_PROCESSCOUNT + ".web", sampleName);
break;
}
diff --git a/browser/extensions/e10srollout/install.rdf.in b/browser/extensions/e10srollout/install.rdf.in
index e9ddce5061f4..613f3e3a90f1 100644
--- a/browser/extensions/e10srollout/install.rdf.in
+++ b/browser/extensions/e10srollout/install.rdf.in
@@ -10,7 +10,7 @@
e10srollout@mozilla.org
- 1.15
+ 1.50
2
true
true
diff --git a/browser/extensions/formautofill/content/manageProfiles.js b/browser/extensions/formautofill/content/manageProfiles.js
index 61c831dbec16..5eed4a2d8159 100644
--- a/browser/extensions/formautofill/content/manageProfiles.js
+++ b/browser/extensions/formautofill/content/manageProfiles.js
@@ -64,6 +64,8 @@ ManageProfileDialog.prototype = {
loadProfiles() {
return this.getProfiles().then(profiles => {
log.debug("profiles:", profiles);
+ // Sort by last modified time starting with most recent
+ profiles.sort((a, b) => b.timeLastModified - a.timeLastModified);
this.renderProfileElements(profiles);
this.updateButtonsStates(this._selectedOptions.length);
});
diff --git a/browser/locales/en-US/chrome/browser/aboutDialog.dtd b/browser/locales/en-US/chrome/browser/aboutDialog.dtd
index e0c34692e85a..2b30e4443991 100644
--- a/browser/locales/en-US/chrome/browser/aboutDialog.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutDialog.dtd
@@ -10,7 +10,7 @@
-->
-
+
diff --git a/browser/locales/en-US/chrome/browser/browser.dtd b/browser/locales/en-US/chrome/browser/browser.dtd
index 6d0281b59a7a..269987a4ad30 100644
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -920,7 +920,7 @@ you can use these alternative items. Otherwise, their values should be empty. -
-
+
diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties
index 90bbbcfdc2ad..7ceceb2f92cd 100644
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -763,11 +763,6 @@ customizeTips.tip0.learnMore = Learn more
# LOCALIZATION NOTE (customizeMode.tabTitle): %S is brandShortName
customizeMode.tabTitle = Customize %S
-# LOCALIZATION NOTE : FILE Reader View is a feature name and therefore typically used as a proper noun.
-
-readingList.promo.firstUse.readerView.title = Reader View
-readingList.promo.firstUse.readerView.body = Remove clutter so you can focus exactly on what you want to read.
-
# LOCALIZATION NOTE (appMenuRemoteTabs.mobilePromo.text2):
# %1$S will be replaced with a link, the text of which is
# appMenuRemoteTabs.mobilePromo.android and the link will be to
diff --git a/browser/locales/shipped-locales b/browser/locales/shipped-locales
index 39bd1cfd099a..adcafab97563 100644
--- a/browser/locales/shipped-locales
+++ b/browser/locales/shipped-locales
@@ -5,6 +5,7 @@ ar
as
ast
az
+be
bg
bn-BD
bn-IN
diff --git a/browser/modules/ContentLinkHandler.jsm b/browser/modules/ContentLinkHandler.jsm
index b4c452556b5f..50298ce2ecc9 100644
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/ContentLinkHandler.jsm
@@ -134,7 +134,7 @@ this.ContentLinkHandler = {
getLinkIconURI(aLink) {
let targetDoc = aLink.ownerDocument;
- var uri = BrowserUtils.makeURI(aLink.href, targetDoc.characterSet);
+ var uri = Services.io.newURI(aLink.href, targetDoc.characterSet);
try {
uri.userPass = "";
} catch (e) {
diff --git a/browser/modules/ReaderParent.jsm b/browser/modules/ReaderParent.jsm
index dc62fcde2b35..e5a1513801dc 100644
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/modules/ReaderParent.jsm
@@ -20,8 +20,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.js
const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
var ReaderParent = {
- _readerModeInfoPanelOpen: false,
-
MESSAGES: [
"Reader:ArticleGet",
"Reader:FaviconRequest",
@@ -108,20 +106,6 @@ var ReaderParent = {
command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey"));
key.setAttribute("disabled", !browser.isArticle);
}
-
- let currentUriHost = browser.currentURI && browser.currentURI.asciiHost;
- if (browser.isArticle &&
- !Services.prefs.getBoolPref("browser.reader.detectedFirstArticle") &&
- currentUriHost && !currentUriHost.endsWith("mozilla.org")) {
- this.showReaderModeInfoPanel(browser);
- Services.prefs.setBoolPref("browser.reader.detectedFirstArticle", true);
- this._readerModeInfoPanelOpen = true;
- } else if (this._readerModeInfoPanelOpen) {
- if (UITour.isInfoOnTarget(win, "readerMode-urlBar")) {
- UITour.hideInfo(win);
- }
- this._readerModeInfoPanelOpen = false;
- }
},
forceShowReaderIcon(browser) {
@@ -142,29 +126,6 @@ var ReaderParent = {
browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
},
- /**
- * Shows an info panel from the UITour for Reader Mode.
- *
- * @param browser The that the tour should be started for.
- */
- showReaderModeInfoPanel(browser) {
- let win = browser.ownerGlobal;
- let targetPromise = UITour.getTarget(win, "readerMode-urlBar");
- targetPromise.then(target => {
- let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
- let icon = "chrome://browser/skin/";
- if (win.devicePixelRatio > 1) {
- icon += "reader-tour@2x.png";
- } else {
- icon += "reader-tour.png";
- }
- UITour.showInfo(win, target,
- browserBundle.GetStringFromName("readingList.promo.firstUse.readerView.title"),
- browserBundle.GetStringFromName("readingList.promo.firstUse.readerView.body"),
- icon);
- });
- },
-
/**
* Gets an article for a given URL. This method will download and parse a document.
*
diff --git a/browser/modules/test/browser/browser.ini b/browser/modules/test/browser/browser.ini
index 782378300051..2e37db598236 100644
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -7,6 +7,7 @@ support-files =
[browser_BrowserUITelemetry_sidebar.js]
[browser_BrowserUITelemetry_syncedtabs.js]
[browser_ContentSearch.js]
+skip-if = (os == "mac" || os == "linux") # Bug 1308343
support-files =
contentSearch.js
contentSearchBadImage.xml
diff --git a/browser/themes/shared/customizableui/panelUI.inc.css b/browser/themes/shared/customizableui/panelUI.inc.css
index 638c4cbc4f52..118d897d5db8 100644
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -1798,11 +1798,3 @@ menuitem[checked="true"].subviewbutton > .menu-iconic-left {
.subviewbutton-iconic > .toolbarbutton-text {
padding-inline-start: 5px;
}
-
-#appMenu-new-window-button {
- list-style-image: url(chrome://browser/skin/menu-icons/new-window.svg);
-}
-
-#appMenu-private-window-button {
- list-style-image: url(chrome://browser/skin/menu-icons/private-window.svg);
-}
diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn
index 4485e54baea8..7ef6edd8124f 100644
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -125,8 +125,6 @@
skin/classic/browser/favicon-search-16.svg (../shared/favicon-search-16.svg)
skin/classic/browser/icon-search-64.svg (../shared/incontent-icons/icon-search-64.svg)
skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
- skin/classic/browser/reader-tour.png (../shared/reader/reader-tour.png)
- skin/classic/browser/reader-tour@2x.png (../shared/reader/reader-tour@2x.png)
skin/classic/browser/readerMode.svg (../shared/reader/readerMode.svg)
skin/classic/browser/panic-panel/header.png (../shared/panic-panel/header.png)
skin/classic/browser/panic-panel/header@2x.png (../shared/panic-panel/header@2x.png)
@@ -146,4 +144,5 @@
skin/classic/browser/urlbar-star.svg (../shared/urlbar-star.svg)
skin/classic/browser/urlbar-tab.svg (../shared/urlbar-tab.svg)
skin/classic/browser/menu-icons/new-window.svg (../shared/menu-icons/new-window.svg)
+ skin/classic/browser/menu-icons/print.svg (../shared/menu-icons/print.svg)
skin/classic/browser/menu-icons/private-window.svg (../shared/menu-icons/private-window.svg)
diff --git a/browser/themes/shared/menu-icons/new-window.svg b/browser/themes/shared/menu-icons/new-window.svg
index 6e834cdd38ad..978b2fd1bcc3 100755
--- a/browser/themes/shared/menu-icons/new-window.svg
+++ b/browser/themes/shared/menu-icons/new-window.svg
@@ -1,3 +1,6 @@
+
diff --git a/browser/themes/shared/menu-icons/print.svg b/browser/themes/shared/menu-icons/print.svg
new file mode 100644
index 000000000000..193607af7306
--- /dev/null
+++ b/browser/themes/shared/menu-icons/print.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/browser/themes/shared/menu-icons/private-window.svg b/browser/themes/shared/menu-icons/private-window.svg
index f4efd68ac100..e56c2920a4d9 100755
--- a/browser/themes/shared/menu-icons/private-window.svg
+++ b/browser/themes/shared/menu-icons/private-window.svg
@@ -1,3 +1,6 @@
+
diff --git a/browser/themes/shared/menupanel.inc.css b/browser/themes/shared/menupanel.inc.css
index ea5b5ccf163b..7cc1f463f5e2 100644
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -181,3 +181,15 @@ toolbarpaletteitem[place="palette"] > #zoom-controls > #zoom-in-button {
-moz-image-region: rect(0px, 96px, 16px, 80px);
}
+
+#appMenu-new-window-button {
+ list-style-image: url(chrome://browser/skin/menu-icons/new-window.svg);
+}
+
+#appMenu-private-window-button {
+ list-style-image: url(chrome://browser/skin/menu-icons/private-window.svg);
+}
+
+#appMenu-print-button {
+ list-style-image: url(chrome://browser/skin/menu-icons/print.svg);
+}
diff --git a/browser/themes/shared/reader/reader-tour.png b/browser/themes/shared/reader/reader-tour.png
deleted file mode 100644
index be346b384792..000000000000
Binary files a/browser/themes/shared/reader/reader-tour.png and /dev/null differ
diff --git a/browser/themes/shared/reader/reader-tour@2x.png b/browser/themes/shared/reader/reader-tour@2x.png
deleted file mode 100644
index 1a60d93ca931..000000000000
Binary files a/browser/themes/shared/reader/reader-tour@2x.png and /dev/null differ
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm
index 087f2595f3cb..e39f0827d38e 100644
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm
@@ -17,41 +17,62 @@ this.Preferences = {
init(libDir) {
let panes = [
- ["paneGeneral", null],
- ["paneSearch", null],
- ["paneContent", null],
- ["paneApplications", null],
- ["panePrivacy", null],
- ["panePrivacy", null, DNTDialog],
- ["panePrivacy", null, clearRecentHistoryDialog],
- ["paneSecurity", null],
- ["paneSync", null],
- ["paneAdvanced", "generalTab"],
- ["paneAdvanced", "dataChoicesTab"],
- ["paneAdvanced", "networkTab"],
- ["paneAdvanced", "networkTab", connectionDialog],
- ["paneAdvanced", "updateTab"],
- ["paneAdvanced", "encryptionTab"],
- ["paneAdvanced", "encryptionTab", certManager],
- ["paneAdvanced", "encryptionTab", deviceManager],
+ /* The "new" organization */
+ ["paneGeneral"],
+ ["paneGeneral", scrollToBrowsingGroup],
+ ["paneApplications"],
+ ["paneSync"],
+ ["panePrivacy"],
+ ["panePrivacy", scrollToCacheGroup],
+ ["panePrivacy", DNTDialog],
+ ["panePrivacy", clearRecentHistoryDialog],
+ ["panePrivacy", connectionDialog],
+ ["panePrivacy", certManager],
+ ["panePrivacy", deviceManager],
+ ["paneAdvanced"],
+
+ /* The "old" organization. The third argument says to
+ set the pref to show the old organization when
+ opening the preferences. */
+ ["paneGeneral", null, true],
+ ["paneSearch", null, true],
+ ["paneContent", null, true],
+ ["paneApplications", null, true],
+ ["panePrivacy", null, true],
+ ["panePrivacy", DNTDialog, true],
+ ["panePrivacy", clearRecentHistoryDialog, true],
+ ["paneSecurity", null, true],
+ ["paneSync", null, true],
+ ["paneAdvanced", null, true, "generalTab"],
+ ["paneAdvanced", null, true, "dataChoicesTab"],
+ ["paneAdvanced", null, true, "networkTab"],
+ ["paneAdvanced", connectionDialog, true, "networkTab"],
+ ["paneAdvanced", null, true, "updateTab"],
+ ["paneAdvanced", null, true, "encryptionTab"],
+ ["paneAdvanced", certManager, true, "encryptionTab"],
+ ["paneAdvanced", deviceManager, true, "encryptionTab"],
];
- for (let [primary, advanced, customFn] of panes) {
+ for (let [primary, customFn, useOldOrg, advanced] of panes) {
let configName = primary.replace(/^pane/, "prefs") + (advanced ? "-" + advanced : "");
if (customFn) {
configName += "-" + customFn.name;
}
this.configurations[configName] = {};
- this.configurations[configName].applyConfig = prefHelper.bind(null, primary, advanced, customFn);
+ this.configurations[configName].applyConfig = prefHelper.bind(null, primary, customFn, useOldOrg, advanced);
}
},
configurations: {},
};
-let prefHelper = Task.async(function*(primary, advanced = null, customFn = null) {
+let prefHelper = Task.async(function*(primary, customFn = null, useOldOrg = false, advanced = null) {
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
let selectedBrowser = browserWindow.gBrowser.selectedBrowser;
+ if (useOldOrg) {
+ Services.prefs.setBoolPref("browser.preferences.useOldOrganization", !!useOldOrg);
+ }
+
// close any dialog that might still be open
yield ContentTask.spawn(selectedBrowser, null, function*() {
if (!content.window.gSubDialog) {
@@ -72,7 +93,7 @@ let prefHelper = Task.async(function*(primary, advanced = null, customFn = null)
readyPromise = TestUtils.topicObserved("advanced-pane-loaded");
}
- if (primary == "paneAdvanced") {
+ if (useOldOrg && primary == "paneAdvanced") {
browserWindow.openAdvancedPreferences(advanced);
} else {
browserWindow.openPreferences(primary);
@@ -85,6 +106,8 @@ let prefHelper = Task.async(function*(primary, advanced = null, customFn = null)
yield* customFn(selectedBrowser);
yield customPaintPromise;
}
+
+ Services.prefs.clearUserPref("browser.preferences.useOldOrganization");
});
function paintPromise(browserWindow) {
@@ -95,6 +118,18 @@ function paintPromise(browserWindow) {
});
}
+function* scrollToBrowsingGroup(aBrowser) {
+ yield ContentTask.spawn(aBrowser, null, function* () {
+ content.document.getElementById("browsingGroup").scrollIntoView();
+ });
+}
+
+function* scrollToCacheGroup(aBrowser) {
+ yield ContentTask.spawn(aBrowser, null, function* () {
+ content.document.getElementById("cacheGroup").scrollIntoView();
+ });
+}
+
function* DNTDialog(aBrowser) {
yield ContentTask.spawn(aBrowser, null, function* () {
content.document.getElementById("doNotTrackSettings").click();
diff --git a/build/moz.configure/toolchain.configure b/build/moz.configure/toolchain.configure
index a18a1d16386c..c336596007c2 100644
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -974,3 +974,16 @@ def is_windows(target, host):
return host.kernel == 'WINNT' and target.kernel == 'WINNT'
include('windows.configure', when=is_windows)
+
+# Security Hardening
+# ==============================================================
+
+option('--enable-hardening', env='MOZ_SECURITY_HARDENING',
+ help='Enables security hardening compiler options')
+
+@depends('--enable-hardening', c_compiler)
+def security_hardening_cflags(value, c_compiler):
+ if value and c_compiler.type in ['gcc', 'clang']:
+ return '-fstack-protector-strong'
+
+add_old_configure_assignment('HARDENING_CFLAGS', security_hardening_cflags)
diff --git a/chrome/nsChromeRegistry.cpp b/chrome/nsChromeRegistry.cpp
index d0e13230875d..00e38c91af10 100644
--- a/chrome/nsChromeRegistry.cpp
+++ b/chrome/nsChromeRegistry.cpp
@@ -55,13 +55,12 @@ nsChromeRegistry::LogMessage(const char* aMsg, ...)
va_list args;
va_start(args, aMsg);
- char* formatted = mozilla::Vsmprintf(aMsg, args);
+ mozilla::SmprintfPointer formatted = mozilla::Vsmprintf(aMsg, args);
va_end(args);
if (!formatted)
return;
- console->LogStringMessage(NS_ConvertUTF8toUTF16(formatted).get());
- mozilla::SmprintfFree(formatted);
+ console->LogStringMessage(NS_ConvertUTF8toUTF16(formatted.get()).get());
}
void
@@ -80,7 +79,7 @@ nsChromeRegistry::LogMessageWithContext(nsIURI* aURL, uint32_t aLineNumber, uint
va_list args;
va_start(args, aMsg);
- char* formatted = mozilla::Vsmprintf(aMsg, args);
+ mozilla::SmprintfPointer formatted = mozilla::Vsmprintf(aMsg, args);
va_end(args);
if (!formatted)
return;
@@ -89,11 +88,10 @@ nsChromeRegistry::LogMessageWithContext(nsIURI* aURL, uint32_t aLineNumber, uint
if (aURL)
aURL->GetSpec(spec);
- rv = error->Init(NS_ConvertUTF8toUTF16(formatted),
+ rv = error->Init(NS_ConvertUTF8toUTF16(formatted.get()),
NS_ConvertUTF8toUTF16(spec),
EmptyString(),
aLineNumber, 0, flags, "chrome registration");
- mozilla::SmprintfFree(formatted);
if (NS_FAILED(rv))
return;
diff --git a/chrome/nsChromeRegistryChrome.cpp b/chrome/nsChromeRegistryChrome.cpp
index 648a6e6e40de..68ef8582d7cd 100644
--- a/chrome/nsChromeRegistryChrome.cpp
+++ b/chrome/nsChromeRegistryChrome.cpp
@@ -740,7 +740,7 @@ nsChromeRegistryChrome::ManifestLocale(ManifestProcessingContext& cx, int lineno
if (strcmp(package, "global") == 0) {
// We should refresh the LocaleService, since the available
// locales changed.
- LocaleService::GetInstance()->Refresh();
+ LocaleService::GetInstance()->OnAvailableLocalesChanged();
}
}
diff --git a/devtools/client/framework/test/browser_toolbox_options.js b/devtools/client/framework/test/browser_toolbox_options.js
index 79d4216617fc..6836278da33c 100644
--- a/devtools/client/framework/test/browser_toolbox_options.js
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -105,15 +105,6 @@ function* testOptionsShortcut() {
function* testOptions() {
let tool = toolbox.getPanel("options");
panelWin = tool.panelWin;
-
- // It's possible that the iframe for options hasn't fully loaded yet,
- // and might be paint-suppressed, which means that clicking things
- // might not work just yet. The "load" event is a good indication that
- // we're ready to proceed.
- if (tool.panelDoc.readyState != "complete") {
- yield once(tool.panelWin, "load");
- }
-
let prefNodes = tool.panelDoc.querySelectorAll(
"input[type=checkbox][data-pref]");
diff --git a/devtools/client/framework/test/browser_toolbox_races.js b/devtools/client/framework/test/browser_toolbox_races.js
index fedbc4402c97..2dca65e94a2f 100644
--- a/devtools/client/framework/test/browser_toolbox_races.js
+++ b/devtools/client/framework/test/browser_toolbox_races.js
@@ -5,6 +5,9 @@
"use strict";
+// Toggling the toolbox three time can take more than 45s on slow test machine
+requestLongerTimeout(2);
+
// Test toggling the toolbox quickly and see if there is any race breaking it.
const URL = "data:text/html;charset=utf-8,Toggling devtools quickly";
diff --git a/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js b/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js
index 3a0855adf714..35a1ca860078 100644
--- a/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js
@@ -4,7 +4,7 @@
"use strict";
-// Test that the box model displays the right values for a pseduo-element.
+// Test that the box model displays the right values for a pseudo-element.
const TEST_URI = `
-
-
-
-
-
-
-
-
+
+
diff --git a/devtools/client/responsive.html/index.css b/devtools/client/responsive.html/index.css
index bae181bbe13a..9c7b321922d6 100644
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -12,11 +12,7 @@
--viewport-color: #999797;
--viewport-hover-color: var(--theme-body-color);
--viewport-active-color: #3b3b3b;
- --viewport-selection-arrow: url("./images/select-arrow.svg#light");
- --viewport-selection-arrow-hovered:
- url("./images/select-arrow.svg#light-hovered");
- --viewport-selection-arrow-selected:
- url("./images/select-arrow.svg#light-selected");
+ --viewport-selection-arrow: url("./images/select-arrow.svg");
}
.theme-dark {
@@ -26,11 +22,7 @@
--viewport-color: #c6ccd0;
--viewport-hover-color: #dde1e4;
--viewport-active-color: #fcfcfc;
- --viewport-selection-arrow: url("./images/select-arrow.svg#dark");
- --viewport-selection-arrow-hovered:
- url("./images/select-arrow.svg#dark-hovered");
- --viewport-selection-arrow-selected:
- url("./images/select-arrow.svg#dark-selected");
+ --viewport-selection-arrow: url("./images/select-arrow.svg");
}
* {
@@ -91,11 +83,13 @@ select {
-moz-appearance: none; appearance: none;
background-color: var(--theme-toolbar-background);
background-image: var(--viewport-selection-arrow);
+ /* uncomment after bug 1350010 lands: context-properties: fill; */
+ fill: currentColor;
+ color: var(--viewport-color);
background-position: 100% 50%;
background-repeat: no-repeat;
background-size: 7px;
border: none;
- color: var(--viewport-color);
height: 100%;
padding: 0 8px;
text-align: center;
@@ -103,12 +97,10 @@ select {
}
select.selected {
- background-image: var(--viewport-selection-arrow-selected);
color: var(--viewport-active-color);
}
select:not(:disabled):hover {
- background-image: var(--viewport-selection-arrow-hovered);
color: var(--viewport-hover-color);
}
@@ -117,7 +109,6 @@ select:not(:disabled):hover {
select is focused. It's unclear whether the visual effect that results here
is intentional and desired. */
select:focus {
- background-image: var(--viewport-selection-arrow-selected);
color: var(--viewport-active-color);
}
@@ -203,12 +194,10 @@ select > option.divider {
}
#global-dpr-selector:not(.disabled):hover > select {
- background-image: var(--viewport-selection-arrow-hovered);
color: var(--viewport-hover-color);
}
#global-dpr-selector:focus > select {
- background-image: var(--viewport-selection-arrow-selected);
color: var(--viewport-active-color);
}
diff --git a/devtools/client/responsivedesign/test/browser.ini b/devtools/client/responsivedesign/test/browser.ini
index 6a8f5a8d912e..9dd08ee654cf 100644
--- a/devtools/client/responsivedesign/test/browser.ini
+++ b/devtools/client/responsivedesign/test/browser.ini
@@ -14,6 +14,7 @@ skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
[browser_responsiveui.js]
[browser_responsiveui_touch.js]
+skip-if = true # Bug 1358261 - Intermittent failures, mostly on Windows
[browser_responsiveuiaddcustompreset.js]
[browser_responsive_devicewidth.js]
[browser_responsiveui_customuseragent.js]
diff --git a/devtools/client/shims/devtools.js b/devtools/client/shims/devtools.js
index 17eae9f8f8af..18c4fee4a47f 100644
--- a/devtools/client/shims/devtools.js
+++ b/devtools/client/shims/devtools.js
@@ -10,6 +10,9 @@
*/
const DevTools = {
chromeWindowType: "navigator:browser",
+ getToolbox: function () {
+ return {};
+ }
};
exports.gDevTools = DevTools;
diff --git a/devtools/client/themes/images/performance-details-call-tree.svg b/devtools/client/themes/images/performance-details-call-tree.svg
new file mode 100644
index 000000000000..3985847ffc62
--- /dev/null
+++ b/devtools/client/themes/images/performance-details-call-tree.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/devtools/client/themes/images/performance-details-flamegraph.svg b/devtools/client/themes/images/performance-details-flamegraph.svg
new file mode 100644
index 000000000000..60bd7a5de043
--- /dev/null
+++ b/devtools/client/themes/images/performance-details-flamegraph.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/devtools/client/themes/images/performance-details-waterfall.svg b/devtools/client/themes/images/performance-details-waterfall.svg
new file mode 100644
index 000000000000..41b4ddc0e899
--- /dev/null
+++ b/devtools/client/themes/images/performance-details-waterfall.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/devtools/client/themes/images/performance-icons.svg b/devtools/client/themes/images/performance-icons.svg
deleted file mode 100644
index d0d5e881e75e..000000000000
--- a/devtools/client/themes/images/performance-icons.svg
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
diff --git a/devtools/client/themes/images/sort-arrows.svg b/devtools/client/themes/images/sort-arrows.svg
deleted file mode 100644
index 289b07530d7e..000000000000
--- a/devtools/client/themes/images/sort-arrows.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
diff --git a/devtools/client/themes/images/sort-ascending-arrow.svg b/devtools/client/themes/images/sort-ascending-arrow.svg
new file mode 100644
index 000000000000..296166592377
--- /dev/null
+++ b/devtools/client/themes/images/sort-ascending-arrow.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/devtools/client/themes/images/sort-descending-arrow.svg b/devtools/client/themes/images/sort-descending-arrow.svg
new file mode 100644
index 000000000000..e43ff87bd364
--- /dev/null
+++ b/devtools/client/themes/images/sort-descending-arrow.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/devtools/client/themes/new-webconsole.css b/devtools/client/themes/new-webconsole.css
new file mode 100644
index 000000000000..a2d1a20e2e1a
--- /dev/null
+++ b/devtools/client/themes/new-webconsole.css
@@ -0,0 +1,596 @@
+
+@import "chrome://devtools/skin/widgets.css";
+@import "resource://devtools/client/themes/light-theme.css";
+
+/* Webconsole specific theme variables */
+.theme-light,
+.theme-firebug {
+ --error-color: #FF0000;
+ --error-background-color: #FFEBEB;
+ --warning-background-color: #FFFFC8;
+}
+
+/* General output styles */
+
+a {
+ -moz-user-focus: normal;
+ -moz-user-input: enabled;
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+/* Workaround for Bug 575675 - FindChildWithRules aRelevantLinkVisited
+ * assertion when loading HTML page with links in XUL iframe */
+*:visited { }
+
+.webconsole-filterbar-wrapper {
+ flex-grow: 0;
+}
+
+.webconsole-filterbar-primary {
+ display: flex;
+}
+
+.devtools-toolbar.webconsole-filterbar-secondary {
+ height: initial;
+}
+
+.webconsole-filterbar-primary .devtools-plaininput {
+ flex: 1 1 100%;
+}
+
+.webconsole-output.hideTimestamps > .message > .timestamp {
+ display: none;
+}
+
+.message.startGroup .message-body > .objectBox-string,
+.message.startGroupCollapsed .message-body > .objectBox-string {
+ color: var(--theme-body-color);
+ font-weight: bold;
+}
+
+.webconsole-output-wrapper .message > .icon {
+ margin: 3px 0 0 0;
+ padding: 0 0 0 6px;
+}
+
+.message.error > .icon::before {
+ background-position: -12px -36px;
+}
+
+.message.warn > .icon::before {
+ background-position: -24px -36px;
+}
+
+.message.info > .icon::before {
+ background-position: -36px -36px;
+}
+
+.message.network .method {
+ margin-inline-end: 5px;
+}
+
+.network .message-flex-body > .message-body {
+ display: flex;
+}
+
+.webconsole-output-wrapper .message .indent {
+ display: inline-block;
+ border-inline-end: solid 1px var(--theme-splitter-color);
+}
+
+.message.startGroup .indent,
+.message.startGroupCollapsed .indent {
+ border-inline-end-color: transparent;
+ margin-inline-end: 5px;
+}
+
+.message.startGroup .icon,
+.message.startGroupCollapsed .icon {
+ display: none;
+}
+
+/* console.table() */
+.new-consoletable {
+ width: 100%;
+ border-collapse: collapse;
+ --consoletable-border: 1px solid var(--table-splitter-color);
+}
+
+.new-consoletable thead,
+.new-consoletable tbody {
+ background-color: var(--theme-body-background);
+}
+
+.new-consoletable th {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+ margin: 0;
+ padding: 5px 0 0;
+ font-weight: inherit;
+ border-inline-end: var(--consoletable-border);
+ border-bottom: var(--consoletable-border);
+}
+
+.new-consoletable tr:nth-of-type(even) {
+ background-color: var(--table-zebra-background);
+}
+
+.new-consoletable td {
+ padding: 3px 4px;
+ min-width: 100px;
+ -moz-user-focus: normal;
+ color: var(--theme-body-color);
+ border-inline-end: var(--consoletable-border);
+ height: 1.25em;
+ line-height: 1.25em;
+}
+
+
+/* Layout */
+.webconsole-output {
+ flex: 1;
+ direction: ltr;
+ overflow: auto;
+ -moz-user-select: text;
+ position: relative;
+}
+
+:root,
+body,
+#app-wrapper {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ overflow: hidden;
+}
+
+#app-wrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+:root, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+}
+
+#app-wrapper {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+#left-wrapper {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+#output-container {
+ flex: 1;
+ overflow: hidden;
+}
+.webconsole-output-wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.message {
+ display: flex;
+ padding: 0 7px;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.message > .prefix,
+.message > .timestamp {
+ flex: none;
+ color: var(--theme-comment);
+ margin: 3px 6px 0 0;
+}
+
+.message > .indent {
+ flex: none;
+}
+
+.message > .icon {
+ flex: none;
+ margin: 3px 6px 0 0;
+ padding: 0 4px;
+ height: 1em;
+ align-self: flex-start;
+}
+
+.theme-firebug .message > .icon {
+ margin: 0;
+ margin-inline-end: 6px;
+}
+
+.theme-firebug .message[severity="error"],
+.theme-light .message.error,
+.theme-firebug .message.error {
+ color: var(--error-color);
+ background-color: var(--error-background-color);
+}
+
+.theme-firebug .message[severity="warn"],
+.theme-light .message.warn,
+.theme-firebug .message.warn {
+ background-color: var(--warning-background-color);
+}
+
+.message > .icon::before {
+ content: "";
+ background-image: url(chrome://devtools/skin/images/webconsole.svg);
+ background-position: 12px 12px;
+ background-repeat: no-repeat;
+ background-size: 72px 60px;
+ width: 12px;
+ height: 12px;
+ display: inline-block;
+}
+
+.theme-light .message > .icon::before {
+ background-image: url(chrome://devtools/skin/images/webconsole.svg#light-icons);
+}
+
+.message > .message-body-wrapper {
+ flex: auto;
+ min-width: 0px;
+ margin: 3px;
+}
+
+/* The red bubble that shows the number of times a message is repeated */
+.message-repeats {
+ -moz-user-select: none;
+ flex: none;
+ margin: 2px 6px;
+ padding: 0 6px;
+ height: 1.25em;
+ color: white;
+ background-color: red;
+ border-radius: 40px;
+ font: message-box;
+ font-size: 0.9em;
+ font-weight: 600;
+}
+
+.message-repeats[value="1"] {
+ display: none;
+}
+
+.message-location {
+ max-width: 40%;
+}
+
+.stack-trace {
+ /* The markup contains extra whitespace to improve formatting of clipboard text.
+ Make sure this whitespace doesn't affect the HTML rendering */
+ white-space: normal;
+}
+
+.stack-trace .frame-link-source,
+.message-location .frame-link-source {
+ /* Makes the file name truncated (and ellipsis shown) on the left side */
+ direction: rtl;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.stack-trace .frame-link-source-inner,
+.message-location .frame-link-source-inner {
+ /* Enforce LTR direction for the file name - fixes bug 1290056 */
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+.stack-trace .frame-link-function-display-name {
+ max-width: 50%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.message-flex-body {
+ display: flex;
+}
+
+.message-body > * {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.message-flex-body > .message-body {
+ display: block;
+ flex: auto;
+}
+#output-container.hideTimestamps > .message {
+ padding-inline-start: 0;
+ margin-inline-start: 7px;
+ width: calc(100% - 7px);
+}
+
+#output-container.hideTimestamps > .message > .timestamp {
+ display: none;
+}
+
+#output-container.hideTimestamps > .message > .indent {
+ background-color: var(--theme-body-background);
+}
+.message:hover {
+ background-color: var(--theme-selection-background-semitransparent) !important;
+}
+.theme-light .message.error {
+ background-color: rgba(255, 150, 150, 0.3);
+}
+
+.theme-dark .message.error {
+ background-color: rgba(235, 83, 104, 0.17);
+}
+
+.console-string {
+ color: var(--theme-highlight-lightorange);
+}
+.theme-selected .console-string,
+.theme-selected .cm-number,
+.theme-selected .cm-variable,
+.theme-selected .kind-ArrayLike {
+ color: #f5f7fa !important; /* Selection Text Color */
+}
+
+
+.message.network.error > .icon::before {
+ background-position: -12px 0;
+}
+.message.network > .message-body {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+
+.message.network .method {
+ flex: none;
+}
+.message.network:not(.navigation-marker) .url {
+ flex: 1 1 auto;
+ /* Make sure the URL is very small initially, let flex change width as needed. */
+ width: 100px;
+ min-width: 5em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.message.network .status {
+ flex: none;
+ margin-inline-start: 6px;
+}
+.message.network.mixed-content .url {
+ color: var(--theme-highlight-red);
+}
+
+.message .learn-more-link {
+ color: var(--theme-highlight-blue);
+ margin: 0 6px;
+}
+
+.message.network .xhr {
+ background-color: var(--theme-body-color-alt);
+ color: var(--theme-body-background);
+ border-radius: 3px;
+ font-weight: bold;
+ font-size: 10px;
+ padding: 2px;
+ line-height: 10px;
+ margin-inline-start: 3px;
+ margin-inline-end: 1ex;
+}
+.message.cssparser > .indent {
+ border-inline-end: solid #00b6f0 6px;
+}
+.message.cssparser.error > .icon::before {
+ background-position: -12px -12px;
+}
+
+.message.cssparser.warn > .icon::before {
+ background-position: -24px -12px;
+}
+.message.exception > .indent {
+ border-inline-end: solid #fb9500 6px;
+}
+
+.message.exception.error > .icon::before {
+ background-position: -12px -24px;
+}
+.message.exception.warn > .icon::before {
+ background-position: -24px -24px;
+}
+.message.console-api > .indent {
+ border-inline-end: solid #cbcbcb 6px;
+}
+
+.message.server > .indent {
+ border-inline-end: solid #90B090 6px;
+}
+
+/* Input and output styles */
+.message.command > .indent,
+.message.result > .indent {
+ border-inline-end: solid #808080 6px;
+}
+
+.message.command > .icon::before {
+ background-position: -48px -36px;
+}
+
+.message.result > .icon::before {
+ background-position: -60px -36px;
+}
+
+
+
+
+/* JSTerm Styles */
+#jsterm-wrapper {
+ flex: 0;
+}
+.jsterm-input-container {
+ background-color: var(--theme-tab-toolbar-background);
+ border-top: 1px solid var(--theme-splitter-color);
+}
+
+.theme-light .jsterm-input-container {
+ /* For light theme use a white background for the input - it looks better
+ than off-white */
+ background-color: #fff;
+ border-top-color: #e0e0e0;
+}
+
+.theme-firebug .jsterm-input-container {
+ border-top: 1px solid #ccc;
+}
+
+.jsterm-input-node,
+.jsterm-complete-node {
+ border: none;
+ padding: 0;
+ padding-inline-start: 20px;
+ margin: 0;
+ -moz-appearance: none; appearance: none;
+ background-color: transparent;
+}
+
+.jsterm-input-node[focused="true"] {
+ background-image: var(--theme-command-line-image-focus);
+ box-shadow: none;
+}
+
+.jsterm-complete-node {
+ color: var(--theme-comment);
+}
+
+.jsterm-input-node {
+ /* Always allow scrolling on input - it auto expands in js by setting height,
+ but don't want it to get bigger than the window. 24px = toolbar height. */
+ max-height: calc(90vh - 24px);
+ background-image: var(--theme-command-line-image);
+ background-repeat: no-repeat;
+ background-size: 16px 16px;
+ background-position: 4px 50%;
+ color: var(--theme-content-color1);
+}
+
+:-moz-any(.jsterm-input-node,
+ .jsterm-complete-node) > .textbox-input-box > .textbox-textarea {
+ overflow-x: hidden;
+ /* Set padding for console input on textbox to make sure it is inlcuded in
+ scrollHeight that is used when resizing JSTerminal's input. Note: textbox
+ default style has important already */
+ padding: 4px 0 !important;
+}
+#webconsole-notificationbox,
+.jsterm-stack-node {
+ width: 100%;
+}
+
+.message.security > .indent {
+ border-inline-end: solid red 6px;
+}
+
+.message.security.error > .icon::before {
+ background-position: -12px -48px;
+}
+
+.message.security.warn > .icon::before {
+ background-position: -24px -48px;
+}
+
+.navigation-marker {
+ color: #aaa;
+ background: linear-gradient(#aaa, #aaa) no-repeat left 50%;
+ background-size: 100% 2px;
+ margin-top: 6px;
+ margin-bottom: 6px;
+ font-size: 0.9em;
+}
+
+.navigation-marker .url {
+ padding-inline-end: 9px;
+ text-decoration: none;
+ background: var(--theme-body-background);
+}
+
+.theme-light .navigation-marker .url {
+ background: #fff;
+}
+
+.stacktrace {
+ display: none;
+ padding: 5px 10px;
+ margin: 5px 0 0 0;
+ overflow-y: auto;
+ border: 1px solid var(--theme-splitter-color);
+ border-radius: 3px;
+}
+
+.theme-light .message.error .stacktrace {
+ background-color: rgba(255, 255, 255, 0.5);
+}
+
+.theme-dark .message.error .stacktrace {
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+.message.open .stacktrace {
+ display: block;
+}
+
+.message .theme-twisty {
+ display: inline-block;
+ vertical-align: middle;
+ margin: 3px 0 0 0;
+ flex-shrink: 0;
+}
+
+/*Do not mirror the twisty because container force to ltr */
+.message .theme-twisty:dir(rtl),
+.message .theme-twisty:-moz-locale-dir(rtl) {
+ transform: none;
+}
+
+.cm-s-mozilla a[class] {
+ font-style: italic;
+ text-decoration: none;
+}
+
+.cm-s-mozilla a[class]:hover,
+.cm-s-mozilla a[class]:focus {
+ text-decoration: underline;
+}
+
+a.learn-more-link.webconsole-learn-more-link {
+ font-style: normal;
+}
+
+/* Open DOMNode in inspector button */
+.open-inspector {
+ background: url(chrome://devtools/skin/images/vview-open-inspector.png) no-repeat 0 0;
+ padding-left: 16px;
+ margin-left: 5px;
+ cursor: pointer;
+}
+
+.elementNode:hover .open-inspector,
+.open-inspector:hover {
+ filter: url(images/filters.svg#checked-icon-state);
+}
+
+.elementNode:hover .open-inspector:active,
+.open-inspector:active {
+ filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
+}
+
diff --git a/devtools/client/themes/performance.css b/devtools/client/themes/performance.css
index 3ccfd6dc7843..28887dc25f98 100644
--- a/devtools/client/themes/performance.css
+++ b/devtools/client/themes/performance.css
@@ -73,17 +73,17 @@
/* Details panel buttons */
#select-waterfall-view {
- list-style-image: url(images/performance-icons.svg#details-waterfall);
+ list-style-image: url(images/performance-details-waterfall.svg);
}
#select-js-calltree-view,
#select-memory-calltree-view {
- list-style-image: url(images/performance-icons.svg#details-call-tree);
+ list-style-image: url(images/performance-details-call-tree.svg);
}
#select-js-flamegraph-view,
#select-memory-flamegraph-view {
- list-style-image: url(images/performance-icons.svg#details-flamegraph);
+ list-style-image: url(images/performance-details-flamegraph.svg);
}
#select-optimizations-view {
diff --git a/devtools/client/themes/widgets.css b/devtools/client/themes/widgets.css
index 9b6f6cbee011..091bba9ce0b8 100644
--- a/devtools/client/themes/widgets.css
+++ b/devtools/client/themes/widgets.css
@@ -1322,11 +1322,11 @@ widgets.css is overwritten. */
}
.table-widget-column-header[sorted=ascending] {
- background-image: url("chrome://devtools/skin/images/sort-arrows.svg#ascending");
+ background-image: url("chrome://devtools/skin/images/sort-ascending-arrow.svg");
}
.table-widget-column-header[sorted=descending] {
- background-image: url("chrome://devtools/skin/images/sort-arrows.svg#descending");
+ background-image: url("chrome://devtools/skin/images/sort-descending-arrow.svg");
}
.theme-dark .table-widget-column[readonly] {
diff --git a/devtools/client/webconsole/.babelrc b/devtools/client/webconsole/.babelrc
index af0f0c3d3521..f30713e4c9ea 100644
--- a/devtools/client/webconsole/.babelrc
+++ b/devtools/client/webconsole/.babelrc
@@ -1,3 +1,7 @@
{
- "presets": ["es2015"]
+ "env": {
+ "test": {
+ "presets": ["es2015"]
+ }
+ }
}
\ No newline at end of file
diff --git a/devtools/client/webconsole/bin/configure.js b/devtools/client/webconsole/bin/configure.js
new file mode 100644
index 000000000000..38922cee175f
--- /dev/null
+++ b/devtools/client/webconsole/bin/configure.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env node */
+
+"use strict";
+
+const fs = require("fs");
+const path = require("path");
+
+function getConfig() {
+ if (process.env.TARGET === "firefox-panel") {
+ return require("../configs/firefox-panel.json");
+ }
+
+ const developmentConfig = require("../configs/development.json");
+
+ let localConfig = {};
+ if (fs.existsSync(path.resolve(__dirname, "../configs/local.json"))) {
+ localConfig = require("../configs/local.json");
+ }
+
+ return Object.assign({}, developmentConfig, localConfig);
+}
+
+module.exports = {
+ getConfig,
+};
diff --git a/devtools/client/webconsole/bin/dev-server.js b/devtools/client/webconsole/bin/dev-server.js
new file mode 100644
index 000000000000..ae0a5c075de9
--- /dev/null
+++ b/devtools/client/webconsole/bin/dev-server.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env node */
+
+"use strict";
+
+const toolbox = require("devtools-launchpad/index");
+const feature = require("devtools-config");
+const { getConfig } = require("./configure");
+
+const envConfig = getConfig();
+
+feature.setConfig(envConfig);
+
+let webpackConfig = require("../webpack.config");
+
+toolbox.startDevServer(envConfig, webpackConfig, __dirname);
diff --git a/devtools/client/webconsole/configs/development.json b/devtools/client/webconsole/configs/development.json
new file mode 100644
index 000000000000..11898a4a1c11
--- /dev/null
+++ b/devtools/client/webconsole/configs/development.json
@@ -0,0 +1,35 @@
+{
+ "title": "Console",
+ "environment": "development",
+ "baseWorkerURL": "http://localhost:8000/public/build/",
+ "host": "",
+ "theme": "light",
+ "dir": "ltr",
+ "features": {
+ },
+ "logging": {
+ "client": false,
+ "firefoxProxy": false,
+ "actions": false
+ },
+ "chrome": {
+ "debug": false,
+ "host": "localhost",
+ "port": 9222
+ },
+ "node": {
+ "debug": false,
+ "host": "localhost",
+ "port": 9229
+ },
+ "firefox": {
+ "webSocketConnection": false,
+ "proxyHost": "localhost:9000",
+ "webSocketHost": "localhost:6080",
+ "mcPath": "./firefox"
+ },
+ "development": {
+ "serverPort": 8000,
+ "examplesPort": 7999
+ }
+}
diff --git a/devtools/client/webconsole/local-dev/index.js b/devtools/client/webconsole/local-dev/index.js
new file mode 100644
index 000000000000..1a28fb5158b9
--- /dev/null
+++ b/devtools/client/webconsole/local-dev/index.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /* eslint-env browser */
+
+"use strict";
+
+const React = require("react");
+const ReactDOM = require("react-dom");
+const { EventEmitter } = require("devtools-modules");
+const { Services: { appinfo, pref } } = require("devtools-modules");
+const { bootstrap } = require("devtools-launchpad");
+
+EventEmitter.decorate(window);
+
+require("../../themes/new-webconsole.css");
+require("../../shared/components/reps/reps.css");
+
+pref("devtools.debugger.remote-timeout", 10000);
+pref("devtools.hud.loglimit", 1000);
+pref("devtools.webconsole.filter.error", true);
+pref("devtools.webconsole.filter.warn", true);
+pref("devtools.webconsole.filter.info", true);
+pref("devtools.webconsole.filter.log", true);
+pref("devtools.webconsole.filter.debug", true);
+pref("devtools.webconsole.filter.css", false);
+pref("devtools.webconsole.filter.net", false);
+pref("devtools.webconsole.filter.netxhr", false);
+pref("devtools.webconsole.ui.filterbar", false);
+pref("devtools.webconsole.inputHistoryCount", 50);
+pref("devtools.webconsole.persistlog", false);
+pref("devtools.webconsole.timestampMessages", false);
+pref("devtools.webconsole.autoMultiline", true);
+
+const NewConsoleOutputWrapper = require("../new-console-output/new-console-output-wrapper");
+const NewWebConsoleFrame = require("../new-webconsole").NewWebConsoleFrame;
+
+// Replicate the DOM that the root component lives within
+const el = document.createElement("div");
+el.style.flex = "1";
+el.innerHTML = `
+
+`;
+document.querySelector("#mount").appendChild(el);
+
+document.documentElement.classList.add("theme-light");
+
+// Copied from netmonitor/index.js:
+window.addEventListener("DOMContentLoaded", () => {
+ for (let link of document.head.querySelectorAll("link")) {
+ link.href = link.href.replace(/(resource|chrome)\:\/\//, "/");
+ }
+
+ if (appinfo.OS === "Darwin") {
+ document.documentElement.setAttribute("platform", "mac");
+ } else if (appinfo.OS === "Linux") {
+ document.documentElement.setAttribute("platform", "linux");
+ } else {
+ document.documentElement.setAttribute("platform", "win");
+ }
+});
+
+let consoleFrame;
+function onConnect(connection) {
+ // If we are on the main dashboard don't render the component
+ if (!connection || !connection.tabConnection || !connection.tabConnection.tabTarget) {
+ return;
+ }
+
+ // Stub out properties that are received from hudservice
+ const owner = {
+ iframeWindow: window,
+ chromeWindow: window,
+ hudId: "hud_0",
+ target: connection.tabConnection.tabTarget,
+ _browserConsole: false,
+ NewConsoleOutputWrapper,
+ };
+ consoleFrame = new NewWebConsoleFrame(owner);
+ consoleFrame.init().then(function () {
+ console.log("NewWebConsoleFrame initialized");
+ });
+}
+
+// This is just a hack until the local dev environment includes jsterm
+window.evaluateJS = function (input) {
+ consoleFrame.webConsoleClient.evaluateJSAsync(`${input}`, function (r) {
+ consoleFrame.newConsoleOutput.dispatchMessageAdd(r);
+ }, {});
+};
+
+bootstrap(React, ReactDOM, el).then(onConnect);
diff --git a/devtools/client/webconsole/local-dev/jsterm-stub.js b/devtools/client/webconsole/local-dev/jsterm-stub.js
new file mode 100644
index 000000000000..e03e29b1e632
--- /dev/null
+++ b/devtools/client/webconsole/local-dev/jsterm-stub.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function JSTerm(webConsoleFrame) {
+ this.hud = webConsoleFrame;
+ this.hudId = this.hud.hudId;
+ this.historyLoaded = new Promise(r => {
+ r();
+ });
+ this.openVariablesView = () => { };
+ this.init = () => { };
+}
+
+module.exports.JSTerm = JSTerm;
diff --git a/devtools/client/webconsole/new-console-output/actions/index.js b/devtools/client/webconsole/new-console-output/actions/index.js
index 5ce76a402a52..08e88e85c0d3 100644
--- a/devtools/client/webconsole/new-console-output/actions/index.js
+++ b/devtools/client/webconsole/new-console-output/actions/index.js
@@ -7,11 +7,11 @@
"use strict";
const actionModules = [
- "enhancers",
- "filters",
- "messages",
- "ui",
-].map(filename => require(`./${filename}`));
+ require("./enhancers"),
+ require("./filters"),
+ require("./messages"),
+ require("./ui"),
+];
const actions = Object.assign({}, ...actionModules);
diff --git a/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
index b6b540920163..a99e08600b8f 100644
--- a/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
+++ b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
@@ -14,82 +14,96 @@
diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js
index 5ab1c0bb4de4..64ce5e5dd168 100644
--- a/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js
@@ -6,7 +6,10 @@
const L10n = require("devtools/client/webconsole/new-console-output/test/fixtures/L10n");
const Utils = {
- L10n
+ L10n,
+ supportsString: function (s) {
+ return s;
+ }
};
module.exports = {
diff --git a/devtools/client/webconsole/new-webconsole.js b/devtools/client/webconsole/new-webconsole.js
new file mode 100644
index 000000000000..82710bf799fb
--- /dev/null
+++ b/devtools/client/webconsole/new-webconsole.js
@@ -0,0 +1,269 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const Services = require("Services");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const { JSTerm } = require("devtools/client/webconsole/jsterm");
+const { WebConsoleConnectionProxy } = require("devtools/client/webconsole/webconsole-connection-proxy");
+
+const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
+
+// XXX: This file is incomplete (see bug 1326937).
+// It's used when loading the webconsole with devtools-launchpad, but will ultimately be
+// the entry point for the new frontend
+
+/**
+ * A WebConsoleFrame instance is an interactive console initialized *per target*
+ * that displays console log data as well as provides an interactive terminal to
+ * manipulate the target's document content.
+ *
+ * The WebConsoleFrame is responsible for the actual Web Console UI
+ * implementation.
+ *
+ * @constructor
+ * @param object webConsoleOwner
+ * The WebConsole owner object.
+ */
+function NewWebConsoleFrame(webConsoleOwner) {
+ this.owner = webConsoleOwner;
+ this.hudId = this.owner.hudId;
+ this.isBrowserConsole = this.owner._browserConsole;
+ this.NEW_CONSOLE_OUTPUT_ENABLED = true;
+ this.window = this.owner.iframeWindow;
+
+ this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this);
+
+ EventEmitter.decorate(this);
+}
+NewWebConsoleFrame.prototype = {
+ /**
+ * Getter for the debugger WebConsoleClient.
+ * @type object
+ */
+ get webConsoleClient() {
+ return this.proxy ? this.proxy.webConsoleClient : null;
+ },
+
+ /**
+ * Initialize the WebConsoleFrame instance.
+ * @return object
+ * A promise object that resolves once the frame is ready to use.
+ */
+ init() {
+ this._initUI();
+ let connectionInited = this._initConnection();
+
+ // Don't reject if the history fails to load for some reason.
+ // This would be fine, the panel will just start with empty history.
+ let allReady = this.jsterm.historyLoaded.catch(() => {}).then(() => {
+ return connectionInited;
+ });
+
+ // This notification is only used in tests. Don't chain it onto
+ // the returned promise because the console panel needs to be attached
+ // to the toolbox before the web-console-created event is receieved.
+ let notifyObservers = () => {
+ let id = WebConsoleUtils.supportsString(this.hudId);
+ if (Services.obs) {
+ Services.obs.notifyObservers(id, "web-console-created");
+ }
+ };
+ allReady.then(notifyObservers, notifyObservers)
+ .then(this.newConsoleOutput.init);
+
+ return allReady;
+ },
+
+ destroy() {
+ if (this._destroyer) {
+ return this._destroyer.promise;
+ }
+
+ this._destroyer = defer();
+
+ Services.prefs.addObserver(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged);
+ this.React = this.ReactDOM = this.FrameView = null;
+
+ let onDestroy = () => {
+ this._destroyer.resolve(null);
+ };
+ if (this.proxy) {
+ this.proxy.disconnect().then(onDestroy);
+ this.proxy = null;
+ } else {
+ onDestroy();
+ }
+
+ return this._destroyer.promise;
+ },
+
+ _onUpdateListeners() {
+
+ },
+
+ logWarningAboutReplacedAPI() {
+
+ },
+
+ /**
+ * Setter for saving of network request and response bodies.
+ *
+ * @param boolean value
+ * The new value you want to set.
+ */
+ setSaveRequestAndResponseBodies: function (value) {
+ if (!this.webConsoleClient) {
+ // Don't continue if the webconsole disconnected.
+ return promise.resolve(null);
+ }
+
+ let deferred = defer();
+ let newValue = !!value;
+ let toSet = {
+ "NetworkMonitor.saveRequestAndResponseBodies": newValue,
+ };
+
+ // Make sure the web console client connection is established first.
+ this.webConsoleClient.setPreferences(toSet, response => {
+ if (!response.error) {
+ this._saveRequestAndResponseBodies = newValue;
+ deferred.resolve(response);
+ } else {
+ deferred.reject(response.error);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Connect to the server using the remote debugging protocol.
+ *
+ * @private
+ * @return object
+ * A promise object that is resolved/reject based on the connection
+ * result.
+ */
+ _initConnection: function () {
+ if (this._initDefer) {
+ return this._initDefer.promise;
+ }
+
+ this._initDefer = defer();
+ this.proxy = new WebConsoleConnectionProxy(this, this.owner.target);
+
+ this.proxy.connect().then(() => {
+ // on success
+ this._initDefer.resolve(this);
+ }, (reason) => {
+ // on failure
+ // TODO Print a message to console
+ this._initDefer.reject(reason);
+ });
+
+ return this._initDefer.promise;
+ },
+
+ _initUI: function () {
+ this.document = this.window.document;
+ this.rootElement = this.document.documentElement;
+
+ this.outputNode = this.document.getElementById("output-container");
+ this.completeNode = this.document.querySelector(".jsterm-complete-node");
+ this.inputNode = this.document.querySelector(".jsterm-input-node");
+
+ this.jsterm = new JSTerm(this);
+ this.jsterm.init();
+
+ let toolbox = gDevTools.getToolbox(this.owner.target);
+
+ // @TODO Remove this once JSTerm is handled with React/Redux.
+ this.window.jsterm = this.jsterm;
+ // @TODO Once the toolbox has been converted to React, see if passing
+ // in JSTerm is still necessary.
+
+ // Handle both launchpad and toolbox loading
+ let Wrapper = this.owner.NewConsoleOutputWrapper || this.window.NewConsoleOutput;
+ this.newConsoleOutput = new Wrapper(
+ this.outputNode, this.jsterm, toolbox, this.owner, this.document);
+
+ // Toggle the timestamp on preference change
+ Services.prefs.addObserver(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged);
+ this._onToolboxPrefChanged();
+ },
+
+ /**
+ * Handler for page location changes.
+ *
+ * @param string uri
+ * New page location.
+ * @param string title
+ * New page title.
+ */
+ onLocationChange: function (uri, title) {
+ this.contentLocation = uri;
+ if (this.owner.onLocationChange) {
+ this.owner.onLocationChange(uri, title);
+ }
+ },
+
+ /**
+ * Release an actor.
+ *
+ * @private
+ * @param string actor
+ * The actor ID you want to release.
+ */
+ _releaseObject: function (actor) {
+ if (this.proxy) {
+ this.proxy.releaseActor(actor);
+ }
+ },
+
+ /**
+ * Called when the message timestamp pref changes.
+ */
+ _onToolboxPrefChanged: function () {
+ let newValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
+ this.newConsoleOutput.dispatchTimestampsToggle(newValue);
+ },
+
+ /**
+ * Handler for the tabNavigated notification.
+ *
+ * @param string event
+ * Event name.
+ * @param object packet
+ * Notification packet received from the server.
+ */
+ handleTabNavigated: function (event, packet) {
+ if (event == "will-navigate") {
+ if (this.persistLog) {
+ // Add a _type to hit convertCachedPacket.
+ packet._type = true;
+ this.newConsoleOutput.dispatchMessageAdd(packet);
+ } else {
+ this.jsterm.clearOutput();
+ }
+ }
+
+ if (packet.url) {
+ this.onLocationChange(packet.url, packet.title);
+ }
+
+ if (event == "navigate" && !packet.nativeConsoleAPI) {
+ this.logWarningAboutReplacedAPI();
+ }
+ },
+};
+
+exports.NewWebConsoleFrame = NewWebConsoleFrame;
diff --git a/devtools/client/webconsole/package.json b/devtools/client/webconsole/package.json
index 34939b61372e..c9635af8ce43 100644
--- a/devtools/client/webconsole/package.json
+++ b/devtools/client/webconsole/package.json
@@ -1,21 +1,35 @@
{
"name": "webconsole",
"version": "0.0.1",
- "devDependencies": {
- "amd-loader": "0.0.5",
- "babel-preset-es2015": "^6.6.0",
- "babel-register": "^6.7.2",
- "cross-env": "^3.1.3",
- "enzyme": "^2.4.1",
- "expect": "^1.16.0",
- "jsdom": "^9.4.1",
- "jsdom-global": "^2.0.0",
- "mocha": "^2.5.3",
- "require-hacker": "^2.1.4",
- "sinon": "^1.17.5"
+ "engines": {
+ "node": ">=6.9.0"
},
"scripts": {
- "postinstall": "cd ../ && npm install && cd webconsole",
- "test": "cross-env NODE_PATH=../../../ mocha new-console-output/test/**/*.test.js --compilers js:babel-register -r jsdom-global/register -r ./new-console-output/test/require-helper.js"
+ "start": "node bin/dev-server",
+ "test": "cross-env NODE_ENV=test NODE_PATH=../../../ mocha new-console-output/test/**/*.test.js --compilers js:babel-register -r jsdom-global/register -r ./new-console-output/test/require-helper.js"
+ },
+ "dependencies": {
+ "amd-loader": "0.0.5",
+ "babel-preset-es2015": "^6.6.0",
+ "babel-register": "^6.24.0",
+ "cross-env": "^3.1.3",
+ "devtools-config": "0.0.12",
+ "devtools-launchpad": "0.0.67",
+ "devtools-modules": "0.0.24",
+ "enzyme": "^2.4.1",
+ "expect": "^1.16.0",
+ "file-loader": "^0.10.1",
+ "immutable": "^3.8.1",
+ "jsdom": "^9.4.1",
+ "jsdom-global": "^2.0.0",
+ "json-loader": "^0.5.4",
+ "mocha": "^2.5.3",
+ "raw-loader": "^0.5.1",
+ "react": "=15.3.2",
+ "react-dom": "=15.3.2",
+ "react-redux": "=5.0.3",
+ "redux": "^3.6.0",
+ "require-hacker": "^2.1.4",
+ "sinon": "^1.17.5"
}
}
diff --git a/devtools/client/webconsole/webconsole-connection-proxy.js b/devtools/client/webconsole/webconsole-connection-proxy.js
index e19c09eba1df..a5c6af69d4b4 100644
--- a/devtools/client/webconsole/webconsole-connection-proxy.js
+++ b/devtools/client/webconsole/webconsole-connection-proxy.js
@@ -6,14 +6,8 @@
"use strict";
-const {Cc, Ci, Cu} = require("chrome");
-
-const {Utils: WebConsoleUtils} =
- require("devtools/client/webconsole/utils");
-const BrowserLoaderModule = {};
-Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
-
-const promise = require("promise");
+const {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
+const defer = require("devtools/shared/defer");
const Services = require("Services");
const STRINGS_URI = "devtools/client/locales/webconsole.properties";
@@ -127,18 +121,17 @@ WebConsoleConnectionProxy.prototype = {
return this._connectDefer.promise;
}
- this._connectDefer = promise.defer();
+ this._connectDefer = defer();
let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT);
- this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
- this._connectTimer.initWithCallback(this._connectionTimeout,
- timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+ this._connectTimer = setTimeout(this._connectionTimeout, timeout);
let connPromise = this._connectDefer.promise;
connPromise.then(() => {
- this._connectTimer.cancel();
+ clearTimeout(this._connectTimer);
this._connectTimer = null;
}, () => {
+ clearTimeout(this._connectTimer);
this._connectTimer = null;
});
@@ -475,7 +468,7 @@ WebConsoleConnectionProxy.prototype = {
return this._disconnecter.promise;
}
- this._disconnecter = promise.defer();
+ this._disconnecter = defer();
if (!this.client) {
this._disconnecter.resolve(null);
diff --git a/devtools/client/webconsole/webpack.config.js b/devtools/client/webconsole/webpack.config.js
new file mode 100644
index 000000000000..9992feabe712
--- /dev/null
+++ b/devtools/client/webconsole/webpack.config.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env node */
+/* eslint max-len: [0] */
+
+"use strict";
+
+const {toolboxConfig} = require("./node_modules/devtools-launchpad/index");
+const { NormalModuleReplacementPlugin } = require("webpack");
+const {getConfig} = require("./bin/configure");
+
+const path = require("path");
+const projectPath = path.join(__dirname, "local-dev");
+
+let webpackConfig = {
+ entry: {
+ console: [path.join(projectPath, "index.js")],
+ },
+
+ module: {
+ loaders: [
+ {
+ test: /\.(png|svg)$/,
+ loader: "file-loader?name=[path][name].[ext]",
+ },
+ ]
+ },
+
+ output: {
+ path: path.join(__dirname, "assets/build"),
+ filename: "[name].js",
+ publicPath: "/assets/build",
+ },
+
+ externals: [
+ {
+ "promise": "var Promise",
+ }
+ ],
+};
+
+webpackConfig.resolve = {
+ alias: {
+ "Services": "devtools-modules/client/shared/shim/Services",
+
+ "devtools/client/webconsole/jsterm": path.join(projectPath, "jsterm-stub"),
+ "devtools/client/webconsole/utils": path.join(__dirname, "new-console-output/test/fixtures/WebConsoleUtils"),
+ "devtools/client/webconsole/new-console-output": path.join(__dirname, "new-console-output"),
+ "devtools/client/webconsole/webconsole-connection-proxy": path.join(__dirname, "webconsole-connection-proxy"),
+
+ "react": path.join(__dirname, "node_modules/react"),
+ "devtools/client/shared/vendor/immutable": "immutable",
+ "devtools/client/shared/vendor/react": "react",
+ "devtools/client/shared/vendor/react-dom": "react-dom",
+ "devtools/client/shared/vendor/react-redux": "react-redux",
+ "devtools/client/shared/vendor/redux": "redux",
+
+ "devtools/client/locales": path.join(__dirname, "../../client/locales/en-US"),
+ "toolkit/locales": path.join(__dirname, "../../../toolkit/locales/en-US"),
+ "devtools/shared/locales": path.join(__dirname, "../../shared/locales/en-US"),
+ "devtools/shared/plural-form": path.join(__dirname, "../../shared/plural-form"),
+ "devtools/shared/l10n": path.join(__dirname, "../../shared/l10n"),
+
+ "devtools/client/framework/devtools": path.join(__dirname, "../../client/shims/devtools"),
+ "devtools/client/framework/menu": "devtools-modules/client/framework/menu",
+ "devtools/client/framework/menu-item": path.join(__dirname, "../../client/framework/menu-item"),
+
+ "devtools/client/shared/components/reps/reps": path.join(__dirname, "../../client/shared/components/reps/reps"),
+ "devtools/client/shared/redux/middleware/thunk": path.join(__dirname, "../../client/shared/redux/middleware/thunk"),
+ "devtools/client/shared/components/stack-trace": path.join(__dirname, "../../client/shared/components/stack-trace"),
+ "devtools/client/shared/source-utils": path.join(__dirname, "../../client/shared/source-utils"),
+ "devtools/client/shared/components/frame": path.join(__dirname, "../../client/shared/components/frame"),
+
+ "devtools/shared/defer": path.join(__dirname, "../../shared/defer"),
+ "devtools/shared/event-emitter": "devtools-modules/shared/event-emitter",
+ "devtools/shared/client/main": path.join(__dirname, "new-console-output/test/fixtures/ObjectClient"),
+ "devtools/shared/platform/clipboard": path.join(__dirname, "../../shared/platform/content/clipboard"),
+ }
+};
+
+const mappings = [
+ [
+ /utils\/menu/, "devtools-launchpad/src/components/shared/menu"
+ ],
+ [
+ /chrome:\/\/devtools\/skin/,
+ (result) => {
+ result.request = result.request
+ .replace("./chrome://devtools/skin", path.join(__dirname, "../themes"));
+ }
+ ],
+ [
+ /chrome:\/\/devtools\/content/,
+ (result) => {
+ result.request = result.request
+ .replace("./chrome://devtools/content", path.join(__dirname, ".."));
+ }
+ ],
+ [
+ /resource:\/\/devtools/,
+ (result) => {
+ result.request = result.request
+ .replace("./resource://devtools/client", path.join(__dirname, ".."));
+ }
+ ],
+];
+
+webpackConfig.plugins = mappings.map(([regex, res]) =>
+ new NormalModuleReplacementPlugin(regex, res));
+
+// Exclude to transpile all scripts in devtools/ but not for this folder
+const basePath = path.join(__dirname, "../../").replace(/\\/g, "\\\\");
+const baseName = path.basename(__dirname);
+webpackConfig.babelExcludes = new RegExp(`^${basePath}(.(?!${baseName}))*$`);
+
+let config = toolboxConfig(webpackConfig, getConfig());
+
+// Remove loaders from devtools-launchpad's webpack.config.js
+// * For svg-inline loader:
+// Webconsole uses file loader to bundle image assets instead of svg-inline loader
+// * For raw loader:
+// devtools/shared/l10n has preloaded raw loader in require.context
+config.module.loaders = config.module.loaders
+ .filter((loader) => !["svg-inline", "raw"].includes(loader.loader));
+
+module.exports = config;
diff --git a/devtools/server/actors/highlighters/simple-outline.js b/devtools/server/actors/highlighters/simple-outline.js
index 52b7cad749e4..3ef064b7306f 100644
--- a/devtools/server/actors/highlighters/simple-outline.js
+++ b/devtools/server/actors/highlighters/simple-outline.js
@@ -5,21 +5,24 @@
"use strict";
const {
- installHelperSheet,
isNodeValid,
addPseudoClassLock,
removePseudoClassLock
} = require("./utils/markup");
+const { loadSheet } = require("devtools/shared/layout/utils");
+
// SimpleOutlineHighlighter's stylesheet
const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
-const SIMPLE_OUTLINE_SHEET = `.__fx-devtools-hide-shortcut__ {
- visibility: hidden !important
- }
- ${HIGHLIGHTED_PSEUDO_CLASS} {
- outline: 2px dashed #F06!important;
- outline-offset: -2px!important
- }`;
+const SIMPLE_OUTLINE_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
+ .__fx-devtools-hide-shortcut__ {
+ visibility: hidden !important
+ }
+ ${HIGHLIGHTED_PSEUDO_CLASS} {
+ outline: 2px dashed #F06!important;
+ outline-offset: -2px!important
+ }`);
+
/**
* The SimpleOutlineHighlighter is a class that has the same API than the
* BoxModelHighlighter, but adds a pseudo-class on the target element itself
@@ -48,7 +51,7 @@ SimpleOutlineHighlighter.prototype = {
if (isNodeValid(node) && (!this.currentNode || node !== this.currentNode)) {
this.hide();
this.currentNode = node;
- installHelperSheet(node.ownerGlobal, SIMPLE_OUTLINE_SHEET);
+ loadSheet(node.ownerGlobal, SIMPLE_OUTLINE_SHEET);
addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
}
return true;
diff --git a/devtools/server/actors/highlighters/utils/markup.js b/devtools/server/actors/highlighters/utils/markup.js
index 997501491741..019d5f2b1634 100644
--- a/devtools/server/actors/highlighters/utils/markup.js
+++ b/devtools/server/actors/highlighters/utils/markup.js
@@ -101,20 +101,6 @@ function isXUL(window) {
}
exports.isXUL = isXUL;
-/**
- * Inject a helper stylesheet in the window.
- */
-var installedHelperSheets = new WeakSet();
-
-function installHelperSheet(win, url = STYLESHEET_URI, type = "agent") {
- if (installedHelperSheets.has(win.document)) {
- return;
- }
- loadSheet(win, url, type);
- installedHelperSheets.add(win.document);
-}
-exports.installHelperSheet = installHelperSheet;
-
/**
* Returns true if a DOM node is "valid", where "valid" means that the node isn't a dead
* object wrapper, is still attached to a document, and is of a given type.
@@ -275,7 +261,7 @@ CanvasFrameAnonymousContentHelper.prototype = {
//
-
-
diff --git a/layout/style/res/forms.css b/layout/style/res/forms.css
index 2f5b82470ee2..c317736afe74 100644
--- a/layout/style/res/forms.css
+++ b/layout/style/res/forms.css
@@ -649,40 +649,8 @@ input[type="checkbox"]:disabled:hover:active {
input[type="checkbox"]:hover:active,
input[type="radio"]:hover:active {
- background-color: ThreeDFace;
- border-style: inset;
-}
-
-input[type="radio"] {
- background-size: calc(100% - 4px) calc(100% - 4px);
-}
-
-input[type="radio"]:checked {
- background-image: url(radio.svg);
-}
-
-input[type="radio"]:disabled:checked {
- background-image: url(radio.svg#disabled);
-}
-
-input[type="checkbox"] {
- background-size: 100% 100%;
-}
-
-input[type="checkbox"]:checked {
- background-image: url(checkmark.svg);
-}
-
-input[type="checkbox"]:disabled:checked {
- background-image: url(checkmark.svg#disabled);
-}
-
-input[type="checkbox"]:indeterminate {
- background-image: url(indeterminate-checkmark.svg);
-}
-
-input[type="checkbox"]:indeterminate:disabled {
- background-image: url(indeterminate-checkmark.svg#disabled);
+ background-color: ThreeDFace ! important;
+ border-style: inset !important;
}
%endif /* defined(MOZ_WIDGET_ANDROID) */
diff --git a/layout/style/res/indeterminate-checkmark.svg b/layout/style/res/indeterminate-checkmark.svg
deleted file mode 100644
index 3749fb37114e..000000000000
--- a/layout/style/res/indeterminate-checkmark.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/layout/style/res/radio.svg b/layout/style/res/radio.svg
deleted file mode 100644
index bab9a142ca33..000000000000
--- a/layout/style/res/radio.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini
index ecfcb573a6c0..fb8f793bdaeb 100644
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -275,11 +275,9 @@ skip-if = toolkit == 'android' #bug 775227
[test_transitions_and_restyles.html]
[test_transitions_and_zoom.html]
[test_transitions_cancel_near_end.html]
-skip-if = stylo # timeout bug 1328499
[test_transitions_computed_values.html]
[test_transitions_computed_value_combinations.html]
[test_transitions_events.html]
-skip-if = stylo # timeout bug 1328499
[test_transitions.html]
skip-if = (android_version == '18' && debug) # bug 1159532
[test_transitions_bug537151.html]
diff --git a/layout/style/test/stylo-failures.md b/layout/style/test/stylo-failures.md
index bf1ec7e64519..aaa101995c8e 100644
--- a/layout/style/test/stylo-failures.md
+++ b/layout/style/test/stylo-failures.md
@@ -36,7 +36,8 @@ to mochitest command.
* test_webkit_device_pixel_ratio.html: -webkit-device-pixel-ratio [3]
* browser_bug453896.js [8]
* Animation support:
- * test_animations.html [19]
+ * test_transitions_and_reframes.html: pseudo frames bug 1331047 [4]
+ * test_animations.html: 6 of them bug 1331047 [25]
* test_animations_dynamic_changes.html [1]
* test_bug716226.html [1]
* OMTA
@@ -55,7 +56,7 @@ to mochitest command.
* test_value_storage.html `frames` [30]
* Property parsing and computation:
* test_property_syntax_errors.html `animation` [20]
- * test_value_storage.html `animation` [91]
+ * test_value_storage.html `animation` [15]
* CSSOM support:
* \@import bug 1352968
* test_bug221428.html [1]
@@ -74,11 +75,9 @@ to mochitest command.
* Transition support:
* test_compute_data_with_start_struct.html `transition` [2]
* test_transitions.html: pseudo elements [10]
- * test_transitions_computed_value_combinations.html [145]
- * test_value_storage.html `transition` [218]
* Events:
* test_animations_event_order.html [2]
-* test_computed_style.html `gradient`: -moz- and -webkit-prefixed gradient values [30]
+* test_computed_style.html `gradient`: -moz- and -webkit-prefixed gradient values [35]
* ... `mask`: mask-image isn't set properly bug 1341667 [10]
* ... `fill`: svg paint should distinguish whether there is fallback bug 1347409 [2]
* ... `stroke`: svg paint should distinguish whether there is fallback bug 1347409 [2]
@@ -88,8 +87,8 @@ to mochitest command.
* test_compute_data_with_start_struct.html `timing-function`: incorrectly computing keywords to bezier function servo/servo#15086 [2]
* \@counter-style support bug 1328319
* test_counter_descriptor_storage.html [1]
- * test_counter_style.html [1]
- * test_rule_insertion.html `@counter-style` [4]
+ * test_counter_style.html [5]
+ * test_rule_insertion.html `@counter-style` [1]
* ... `cjk-decimal` [1]
* test_value_storage.html `symbols(` [30]
* ... `list-style-type` [60]
@@ -109,7 +108,7 @@ to mochitest command.
* test_descriptor_storage.html [1]
* test_font_face_parser.html `@font-face` [8]
* @namespace support:
- * test_namespace_rule.html: bug 1355715 [17]
+ * test_namespace_rule.html: bug 1355715 [16]
* test_dont_use_document_colors.html: support of disabling document color bug 1355716 [21]
* test_exposed_prop_accessors.html: mainly various unsupported properties [*]
* test_font_feature_values_parsing.html: \@font-feature-values support bug 1355721 [107]
@@ -177,8 +176,11 @@ to mochitest command.
* test_value_storage.html `-moz-linear-gradient` [322]
* ... `-moz-radial-gradient` [309]
* ... `-moz-repeating-` [298]
+ * test_specified_value_serialization.html `-moz-linear-gradient` [2]
* webkit-prefixed gradient functions servo/servo#15441
* test_value_storage.html `-webkit-gradient` [225]
+ * test_specified_value_serialization.html `-webkit-linear-gradient` [1]
+ * test_specified_value_serialization.html `-webkit-radial-gradient` [1]
* moz-prefixed intrinsic width values bug 1355402
* test_box_size_keywords.html [16]
* test_flexbox_flex_shorthand.html `-moz-fit-content` [4]
@@ -188,9 +190,6 @@ to mochitest command.
* ... `-moz-available` [4]
* several prefixed values in cursor property bug 1356072
* test_value_storage.html `cursor` [4]
- * moz-prefixed values of overflow shorthand bug 1330888
- * test_bug319381.html [8]
- * test_value_storage.html `'overflow` [8]
* -webkit-{flex,inline-flex} for display servo/servo#15400
* test_webkit_flex_display.html [4]
* test_pixel_lengths.html `mozmm`: mozmm unit bug 1356104 [3]
@@ -279,7 +278,7 @@ to mochitest command.
* test_css_supports.html: issues around @supports syntax servo/servo#15482 [8]
* test_author_specified_style.html: support serializing color as author specified bug 1348165 [27]
* browser_newtab_share_rule_processors.js: agent style sheet sharing [1]
-* test_selectors.html `this_better_be_unvisited`: visited handling [2]
+* test_selectors.html `this_better_be_unvisited`: visited handling [1]
* test_selectors.html `:nth-child`: <an+b> parsing difference servo/rust-cssparser#138 [14]
## Assertions
diff --git a/layout/style/test/test_computed_style.html b/layout/style/test/test_computed_style.html
index 4af7c5f4dafc..5a7243f5abd2 100644
--- a/layout/style/test/test_computed_style.html
+++ b/layout/style/test/test_computed_style.html
@@ -530,6 +530,52 @@ var noframe_container = document.getElementById("content");
p.remove();
})();
+(function test_bug_1357117() {
+ // Test that vendor-prefixed gradient styles round-trip with the same prefix,
+ // or with no prefix.
+ var backgroundImages = [
+ // [ specified style,
+ // expected computed style,
+ // descriptionOfTestcase ],
+ // Linear gradient with legacy-gradient-line (needs prefixed syntax):
+ [ "-moz-linear-gradient(10deg, red, blue)",
+ "-moz-linear-gradient(10deg, rgb(255, 0, 0), rgb(0, 0, 255))",
+ "-moz-linear-gradient with angled legacy-gradient-line" ],
+ [ "-webkit-linear-gradient(10deg, red, blue)",
+ "-webkit-linear-gradient(10deg, rgb(255, 0, 0), rgb(0, 0, 255))",
+ "-webkit-linear-gradient with angled legacy-gradient-line" ],
+
+ // Linear gradient with box corner (needs prefixed syntax):
+ [ "-moz-linear-gradient(top left, red, blue)",
+ "-moz-linear-gradient(0% 0%, rgb(255, 0, 0), rgb(0, 0, 255))",
+ "-moz-linear-gradient with box corner" ],
+ [ "-webkit-linear-gradient(top left, red, blue)",
+ "-webkit-linear-gradient(top left, rgb(255, 0, 0), rgb(0, 0, 255))",
+ "-webkit-linear-gradient with box corner" ],
+
+ // Radial gradients (should be serialized using modern unprefixed style):
+ [ "-moz-radial-gradient(contain, red, blue)",
+ "radial-gradient(closest-side, rgb(255, 0, 0), rgb(0, 0, 255))",
+ "-moz-radial-gradient with legacy 'contain' keyword" ],
+ [ "-webkit-radial-gradient(contain, red, blue)",
+ "radial-gradient(closest-side, rgb(255, 0, 0), rgb(0, 0, 255))",
+ "-webkit-radial-gradient with legacy 'contain' keyword" ],
+ ];
+
+ var p = document.createElement("p");
+ var cs = getComputedStyle(p, "");
+ frame_container.appendChild(p);
+
+ for (var i = 0; i < backgroundImages.length; ++i) {
+ var test = backgroundImages[i];
+ p.style.backgroundImage = test[0];
+ is(cs.backgroundImage, test[1],
+ "computed value of prefixed gradient expression (" + test[2] + ")");
+ }
+
+ p.remove();
+})();
+