diff --git a/Makefile.in b/Makefile.in index 6ded78881677..6d16962b0926 100644 --- a/Makefile.in +++ b/Makefile.in @@ -249,6 +249,9 @@ endif # MOZ_CRASHREPORTER uploadsymbols: ifdef MOZ_CRASHREPORTER +ifdef SOCORRO_SYMBOL_UPLOAD_TOKEN_FILE + $(PYTHON) $(topsrcdir)/toolkit/crashreporter/tools/upload_symbols.py '$(DIST)/$(PKG_PATH)$(SYMBOL_FULL_ARCHIVE_BASENAME).zip' +endif $(SHELL) $(topsrcdir)/toolkit/crashreporter/tools/upload_symbols.sh $(SYMBOL_INDEX_NAME) '$(DIST)/$(PKG_PATH)$(SYMBOL_FULL_ARCHIVE_BASENAME).zip' endif diff --git a/accessible/base/ARIAMap.cpp b/accessible/base/ARIAMap.cpp index 4cc313e6db88..a6f8b93f2985 100644 --- a/accessible/base/ARIAMap.cpp +++ b/accessible/base/ARIAMap.cpp @@ -540,6 +540,17 @@ static nsRoleMapEntry sWAIRoleMaps[] = kGenericAccType, kNoReqStates }, + { // switch + &nsGkAtoms::_switch, + roles::SWITCH, + kUseMapRole, + eNoValue, + eCheckUncheckAction, + eNoLiveAttr, + kGenericAccType, + kNoReqStates, + eARIACheckableBool + }, { // tab &nsGkAtoms::tab, roles::PAGETAB, diff --git a/accessible/base/AccEvent.cpp b/accessible/base/AccEvent.cpp index 363d1b82cb38..e4daf091b6c1 100644 --- a/accessible/base/AccEvent.cpp +++ b/accessible/base/AccEvent.cpp @@ -139,7 +139,7 @@ AccTextSelChangeEvent::~AccTextSelChangeEvent() { } bool AccTextSelChangeEvent::IsCaretMoveOnly() const { - return mSel->GetRangeCount() == 1 && mSel->IsCollapsed() && + return mSel->RangeCount() == 1 && mSel->IsCollapsed() && ((mReason & (nsISelectionListener::COLLAPSETOSTART_REASON | nsISelectionListener::COLLAPSETOEND_REASON)) == 0); } diff --git a/accessible/base/Role.h b/accessible/base/Role.h index 0d5746f3dc13..1566eb9f9c53 100644 --- a/accessible/base/Role.h +++ b/accessible/base/Role.h @@ -785,7 +785,12 @@ enum Role { */ KEY = 129, - LAST_ROLE = KEY + /** + * Represent a switch control widget (ARIA role "switch"). + */ + SWITCH = 130, + + LAST_ROLE = SWITCH }; } // namespace role diff --git a/accessible/base/RoleMap.h b/accessible/base/RoleMap.h index 70a3721b1f67..b3195cfd5b67 100644 --- a/accessible/base/RoleMap.h +++ b/accessible/base/RoleMap.h @@ -1055,3 +1055,11 @@ ROLE(KEY, ROLE_SYSTEM_PUSHBUTTON, ROLE_SYSTEM_PUSHBUTTON, eNameFromSubtreeRule) + +ROLE(SWITCH, + "switch", + ATK_ROLE_TOGGLE_BUTTON, + NSAccessibilityCheckBoxRole, + ROLE_SYSTEM_CHECKBUTTON, + IA2_ROLE_TOGGLE_BUTTON, + eNameFromSubtreeRule) diff --git a/accessible/generic/HyperTextAccessible-inl.h b/accessible/generic/HyperTextAccessible-inl.h index 81e421536839..5d12b21bac75 100644 --- a/accessible/generic/HyperTextAccessible-inl.h +++ b/accessible/generic/HyperTextAccessible-inl.h @@ -49,7 +49,7 @@ HyperTextAccessible::AddToSelection(int32_t aStartOffset, int32_t aEndOffset) { dom::Selection* domSel = DOMSelection(); return domSel && - SetSelectionBoundsAt(domSel->GetRangeCount(), aStartOffset, aEndOffset); + SetSelectionBoundsAt(domSel->RangeCount(), aStartOffset, aEndOffset); } inline void diff --git a/accessible/generic/HyperTextAccessible.cpp b/accessible/generic/HyperTextAccessible.cpp index e041f77f490e..7337d261dc5f 100644 --- a/accessible/generic/HyperTextAccessible.cpp +++ b/accessible/generic/HyperTextAccessible.cpp @@ -1169,7 +1169,7 @@ HyperTextAccessible::SetSelectionRange(int32_t aStartPos, int32_t aEndPos) NS_ENSURE_STATE(domSel); // Set up the selection. - for (int32_t idx = domSel->GetRangeCount() - 1; idx > 0; idx--) + for (int32_t idx = domSel->RangeCount() - 1; idx > 0; idx--) domSel->RemoveRange(domSel->GetRangeAt(idx)); SetSelectionBoundsAt(0, aStartPos, aEndPos); @@ -1474,7 +1474,7 @@ HyperTextAccessible::SetSelectionBoundsAt(int32_t aSelectionNum, return false; nsRefPtr range; - uint32_t rangeCount = domSel->GetRangeCount(); + uint32_t rangeCount = domSel->RangeCount(); if (aSelectionNum == static_cast(rangeCount)) range = new nsRange(mContent); else @@ -1502,7 +1502,7 @@ HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) if (!domSel) return false; - if (aSelectionNum < 0 || aSelectionNum >= domSel->GetRangeCount()) + if (aSelectionNum < 0 || aSelectionNum >= static_cast(domSel->RangeCount())) return false; domSel->RemoveRange(domSel->GetRangeAt(aSelectionNum)); @@ -1948,7 +1948,7 @@ HyperTextAccessible::GetSpellTextAttr(nsINode* aNode, if (!domSel) return; - int32_t rangeCount = domSel->GetRangeCount(); + int32_t rangeCount = domSel->RangeCount(); if (rangeCount <= 0) return; diff --git a/accessible/interfaces/nsIAccessibleRole.idl b/accessible/interfaces/nsIAccessibleRole.idl index 81f5314f9716..aeb1d6672c5f 100644 --- a/accessible/interfaces/nsIAccessibleRole.idl +++ b/accessible/interfaces/nsIAccessibleRole.idl @@ -8,7 +8,7 @@ /** * Defines cross platform (Gecko) roles. */ -[scriptable, uuid(50db5e86-9a45-4637-a5c3-4ff148c33270)] +[scriptable, uuid(76ce835f-ef86-47c0-ac7b-e871417f1b6e)] interface nsIAccessibleRole : nsISupports { /** @@ -778,4 +778,9 @@ interface nsIAccessibleRole : nsISupports * A keyboard or keypad key. */ const unsigned long ROLE_KEY = 129; + + /** + * A switch control widget. + */ + const unsigned long ROLE_SWITCH = 130; }; diff --git a/accessible/mac/mozAccessible.mm b/accessible/mac/mozAccessible.mm index 81db5ba136f3..1df45119a6e6 100644 --- a/accessible/mac/mozAccessible.mm +++ b/accessible/mac/mozAccessible.mm @@ -475,6 +475,9 @@ GetClosestInterestingAccessible(id anObject) case roles::DEFINITION: return @"AXDefinition"; + case roles::SWITCH: + return @"AXSwitch"; + default: break; } diff --git a/accessible/tests/mochitest/actions/test_aria.html b/accessible/tests/mochitest/actions/test_aria.html index 5a5f4259e067..c4eae812dac2 100644 --- a/accessible/tests/mochitest/actions/test_aria.html +++ b/accessible/tests/mochitest/actions/test_aria.html @@ -85,6 +85,16 @@ actionName: "select", events: CLICK_EVENTS }, + { + ID: "switch_unchecked", + actionName: "check", + events: CLICK_EVENTS + }, + { + ID: "switch_checked", + actionName: "uncheck", + events: CLICK_EVENTS + }, { ID: "tab", actionName: "switch", @@ -166,6 +176,10 @@ +
Switch
+ +
Switch
+
diff --git a/accessible/tests/mochitest/events/test_aria_statechange.html b/accessible/tests/mochitest/events/test_aria_statechange.html index 3ae98e7fef63..945fde2a859e 100644 --- a/accessible/tests/mochitest/events/test_aria_statechange.html +++ b/accessible/tests/mochitest/events/test_aria_statechange.html @@ -107,17 +107,28 @@ new setAttrOfMixedType(aID, "aria-checked", STATE_CHECKED, aValue); } + function buildQueueForAttr(aList, aQueue, aID, aInvokerFunc) + { + for (var i = 0; i < aList.length; i++) { + for (var j = i + 1; j < aList.length; j++) { + // XXX: changes from/to "undefined"/"" shouldn't fire state change + // events, bug 472142. + aQueue.push(new aInvokerFunc(aID, aList[i])); + aQueue.push(new aInvokerFunc(aID, aList[j])); + } + } + } + function buildQueueForAttrOfMixedType(aQueue, aID, aInvokerFunc) { var list = [ "", "undefined", "false", "true", "mixed" ]; - for (var i = 0; i < list.length; i++) { - for (var j = i + 1; j < list.length; j++) { - // XXX: changes from/to "undefined"/"" shouldn't fire state change - // events, bug 472142. - aQueue.push(new aInvokerFunc(aID, list[i])); - aQueue.push(new aInvokerFunc(aID, list[j])); - } - } + buildQueueForAttr(list, aQueue, aID, aInvokerFunc); + } + + function buildQueueForAttrOfBoolType(aQueue, aID, aInvokerFunc) + { + var list = [ "", "undefined", "false", "true" ]; + buildQueueForAttr(list, aQueue, aID, aInvokerFunc); } function doTests() @@ -135,6 +146,7 @@ buildQueueForAttrOfMixedType(gQueue, "pressable", setPressed); buildQueueForAttrOfMixedType(gQueue, "pressable_native", setPressed); buildQueueForAttrOfMixedType(gQueue, "checkable", setChecked); + buildQueueForAttrOfBoolType(gQueue, "checkableBool", setChecked); gQueue.invoke(); // Will call SimpleTest.finish(); } @@ -166,6 +178,11 @@ title="Pressed state is not exposed on a button element with aria-pressed attribute" Mozilla Bug 989958 +

@@ -186,5 +203,6 @@ +
diff --git a/accessible/tests/mochitest/role.js b/accessible/tests/mochitest/role.js index 3a217112fdab..08cb39e63465 100644 --- a/accessible/tests/mochitest/role.js +++ b/accessible/tests/mochitest/role.js @@ -70,6 +70,7 @@ const ROLE_SLIDER = nsIAccessibleRole.ROLE_SLIDER; const ROLE_SPINBUTTON = nsIAccessibleRole.ROLE_SPINBUTTON; const ROLE_STATICTEXT = nsIAccessibleRole.ROLE_STATICTEXT; const ROLE_STATUSBAR = nsIAccessibleRole.ROLE_STATUSBAR; +const ROLE_SWITCH = nsIAccessibleRole.ROLE_SWITCH; const ROLE_TABLE = nsIAccessibleRole.ROLE_TABLE; const ROLE_TERM = nsIAccessibleRole.ROLE_TERM; const ROLE_TEXT_CONTAINER = nsIAccessibleRole.ROLE_TEXT_CONTAINER; diff --git a/accessible/tests/mochitest/role/test_aria.html b/accessible/tests/mochitest/role/test_aria.html index 11361aa901c2..c110b18195a9 100644 --- a/accessible/tests/mochitest/role/test_aria.html +++ b/accessible/tests/mochitest/role/test_aria.html @@ -60,6 +60,7 @@ testRole("aria_slider", ROLE_SLIDER); testRole("aria_spinbutton", ROLE_SPINBUTTON); testRole("aria_status", ROLE_STATUSBAR); + testRole("aria_switch", ROLE_SWITCH); testRole("aria_tab", ROLE_PAGETAB); testRole("aria_tablist", ROLE_PAGETABLIST); testRole("aria_tabpanel", ROLE_PROPERTYPAGE); @@ -178,6 +179,11 @@ href="https://bugzilla.mozilla.org/show_bug.cgi?id=735645"> Bug 735645
+

@@ -225,6 +231,7 @@
   
   
   
+  
   
   
   
diff --git a/accessible/tests/mochitest/states/test_aria.html b/accessible/tests/mochitest/states/test_aria.html
index 35350eed4f5f..d614a46aaf5a 100644
--- a/accessible/tests/mochitest/states/test_aria.html
+++ b/accessible/tests/mochitest/states/test_aria.html
@@ -92,6 +92,8 @@
       // aria-checked
       testStates("aria_checked_checkbox", STATE_CHECKED);
       testStates("aria_mixed_checkbox", STATE_MIXED);
+      testStates("aria_checked_switch", STATE_CHECKED);
+      testStates("aria_mixed_switch", 0, 0, STATE_MIXED); // unsupported
 
       // test disabled group and all its descendants to see if they are
       // disabled, too. See bug 429285.
@@ -350,6 +352,11 @@
      title="Pressed state is not exposed on a button element with aria-pressed attribute"
     Mozilla Bug 989958
   
+  
 
   

@@ -383,6 +390,15 @@ + +
+ I am switched on +
+ +
+ I am unsupported +
+
modal stuff
non modal stuff
div>
diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in index 8facd7e5debb..f759b666cbbc 100644 --- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -537,6 +537,8 @@ @BINPATH@/components/formautofill.manifest @BINPATH@/components/FormAutofillContentService.js @BINPATH@/components/FormAutofillStartup.js +@BINPATH@/components/CSSUnprefixingService.js +@BINPATH@/components/CSSUnprefixingService.manifest @BINPATH@/components/contentAreaDropListener.manifest @BINPATH@/components/contentAreaDropListener.js @BINPATH@/components/messageWakeupService.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 26910d4cb9ce..a637393682f1 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1577,6 +1577,9 @@ pref("devtools.browserconsole.filter.secwarn", true); // Text size in the Web Console. Use 0 for the system default size. pref("devtools.webconsole.fontSize", 0); +// Max number of inputs to store in web console history. +pref("devtools.webconsole.inputHistoryCount", 50); + // Persistent logging: |true| if you want the Web Console to keep all of the // logged messages after reloading the page, |false| if you want the output to // be cleared each time page navigation happens. diff --git a/browser/base/content/browser-syncui.js b/browser/base/content/browser-syncui.js index f9fe95bb300b..d956efc80da2 100644 --- a/browser/base/content/browser-syncui.js +++ b/browser/base/content/browser-syncui.js @@ -11,13 +11,19 @@ XPCOMUtils.defineLazyModuleGetter(this, "CloudSync", let CloudSync = null; #endif +XPCOMUtils.defineLazyModuleGetter(this, "ReadingListScheduler", + "resource:///modules/readinglist/Scheduler.jsm"); + // gSyncUI handles updating the tools menu and displaying notifications. let gSyncUI = { _obs: ["weave:service:sync:start", + "weave:service:sync:finish", + "weave:service:sync:error", "weave:service:quota:remaining", "weave:service:setup-complete", "weave:service:login:start", "weave:service:login:finish", + "weave:service:login:error", "weave:service:logout:finish", "weave:service:start-over", "weave:service:start-over:finish", @@ -25,9 +31,15 @@ let gSyncUI = { "weave:ui:sync:error", "weave:ui:sync:finish", "weave:ui:clear-error", + + "readinglist:sync:start", + "readinglist:sync:finish", + "readinglist:sync:error", ], _unloaded: false, + // The number of "active" syncs - while this is non-zero, our button will spin + _numActiveSyncTasks: 0, init: function () { Cu.import("resource://services-common/stringbundle.js"); @@ -95,21 +107,25 @@ let gSyncUI = { } }, - _needsSetup: function SUI__needsSetup() { + _needsSetup() { // We want to treat "account needs verification" as "needs setup". So // "reach in" to Weave.Status._authManager to check whether we the signed-in // user is verified. // Referencing Weave.Status spins a nested event loop to initialize the // authManager, so this should always return a value directly. // This only applies to fxAccounts-based Sync. - if (Weave.Status._authManager._signedInUser) { - // If we have a signed in user already, and that user is not verified, - // revert to the "needs setup" state. - if (!Weave.Status._authManager._signedInUser.verified) { - return true; - } + if (Weave.Status._authManager._signedInUser !== undefined) { + // So we are using Firefox accounts - in this world, checking Sync isn't + // enough as reading list may be configured but not Sync. + // We consider ourselves setup if we have a verified user. + // XXX - later we should consider checking preferences to ensure at least + // one engine is enabled? + return !Weave.Status._authManager._signedInUser || + !Weave.Status._authManager._signedInUser.verified; } + // So we are using legacy sync, and reading-list isn't supported for such + // users, so check sync itself. let firstSync = ""; try { firstSync = Services.prefs.getCharPref("services.sync.firstSync"); @@ -120,7 +136,8 @@ let gSyncUI = { }, _loginFailed: function () { - return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; + return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED || + ReadingListScheduler.state == ReadingListScheduler.STATE_ERROR_AUTHENTICATION; }, updateUI: function SUI_updateUI() { @@ -136,6 +153,7 @@ let gSyncUI = { document.getElementById("sync-syncnow-state").hidden = false; } else if (loginFailed) { document.getElementById("sync-reauth-state").hidden = false; + this.showLoginError(); } else if (needsSetup) { document.getElementById("sync-setup-state").hidden = false; } else { @@ -146,14 +164,6 @@ let gSyncUI = { return; let syncButton = document.getElementById("sync-button"); - if (syncButton) { - syncButton.removeAttribute("status"); - } - let panelHorizontalButton = document.getElementById("PanelUI-fxa-status"); - if (panelHorizontalButton) { - panelHorizontalButton.removeAttribute("syncstatus"); - } - if (needsSetup && syncButton) syncButton.removeAttribute("tooltiptext"); @@ -162,17 +172,45 @@ let gSyncUI = { // Functions called by observers - onActivityStart: function SUI_onActivityStart() { + onActivityStart() { if (!gBrowser) return; - let button = document.getElementById("sync-button"); - if (button) { - button.setAttribute("status", "active"); + this.log.debug("onActivityStart with numActive", this._numActiveSyncTasks); + if (++this._numActiveSyncTasks == 1) { + let button = document.getElementById("sync-button"); + if (button) { + button.setAttribute("status", "active"); + } + button = document.getElementById("PanelUI-fxa-status"); + if (button) { + button.setAttribute("syncstatus", "active"); + } } - button = document.getElementById("PanelUI-fxa-status"); - if (button) { - button.setAttribute("syncstatus", "active"); + }, + + onActivityStop() { + if (!gBrowser) + return; + this.log.debug("onActivityStop with numActive", this._numActiveSyncTasks); + if (--this._numActiveSyncTasks) { + if (this._numActiveSyncTasks < 0) { + // This isn't particularly useful (it seems more likely we'll set a + // "start" without a "stop" meaning it forever remains > 0) but it + // might offer some value... + this.log.error("mismatched onActivityStart/Stop calls", + new Error("active=" + this._numActiveSyncTasks)); + } + return; // active tasks are still ongoing... + } + + let syncButton = document.getElementById("sync-button"); + if (syncButton) { + syncButton.removeAttribute("status"); + } + let panelHorizontalButton = document.getElementById("PanelUI-fxa-status"); + if (panelHorizontalButton) { + panelHorizontalButton.removeAttribute("syncstatus"); } }, @@ -187,6 +225,7 @@ let gSyncUI = { }, onLoginError: function SUI_onLoginError() { + // Note: This is used for *both* Sync and ReadingList login errors. // if login fails, any other notifications are essentially moot Weave.Notifications.removeAll(); @@ -200,11 +239,18 @@ let gSyncUI = { this.updateUI(); return; } + this.showLoginError(); + this.updateUI(); + }, + showLoginError() { + // Note: This is used for *both* Sync and ReadingList login errors. let title = this._stringBundle.GetStringFromName("error.login.title"); let description; - if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) { + if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE || + this.isProlongedReadingListError()) { + this.log.debug("showLoginError has a prolonged login error"); // Convert to days let lastSync = Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400; @@ -214,6 +260,7 @@ let gSyncUI = { let reason = Weave.Utils.getErrorString(Weave.Status.login); description = this._stringBundle.formatStringFromName("error.sync.description", [reason], 1); + this.log.debug("showLoginError has a non-prolonged error", reason); } let buttons = []; @@ -226,7 +273,6 @@ let gSyncUI = { let notification = new Weave.Notification(title, description, null, Weave.Notifications.PRIORITY_WARNING, buttons); Weave.Notifications.replaceTitle(notification); - this.updateUI(); }, onLogout: function SUI_onLogout() { @@ -271,6 +317,7 @@ let gSyncUI = { } Services.obs.notifyObservers(null, "cloudsync:user-sync", null); + Services.obs.notifyObservers(null, "readinglist:user-sync", null); }, handleToolbarButton: function SUI_handleStatusbarButton() { @@ -367,7 +414,15 @@ let gSyncUI = { let lastSync; try { - lastSync = Services.prefs.getCharPref("services.sync.lastSync"); + lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync")); + } + catch (e) { }; + // and reading-list time - we want whatever one is the most recent. + try { + let lastRLSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync")); + if (!lastSync || lastRLSync > lastSync) { + lastSync = lastRLSync; + } } catch (e) { }; if (!lastSync || this._needsSetup()) { @@ -376,9 +431,9 @@ let gSyncUI = { } // Show the day-of-week and time (HH:MM) of last sync - let lastSyncDate = new Date(lastSync).toLocaleFormat("%a %H:%M"); + let lastSyncDateString = lastSync.toLocaleFormat("%a %H:%M"); let lastSyncLabel = - this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDate], 1); + this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1); syncButton.setAttribute("tooltiptext", lastSyncLabel); }, @@ -395,7 +450,69 @@ let gSyncUI = { this.clearError(title); }, + // Return true if the reading-list is in a "prolonged" error state. That + // engine doesn't impose what that means, so calculate it here. For + // consistency, we just use the sync prefs. + isProlongedReadingListError() { + let lastSync, threshold, prolonged; + try { + lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync")); + threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout")); + prolonged = lastSync <= threshold; + } catch (ex) { + // no pref, assume not prolonged. + prolonged = false; + } + this.log.debug("isProlongedReadingListError has last successful sync at ${lastSync}, threshold is ${threshold}, prolonged=${prolonged}", + {lastSync, threshold, prolonged}); + return prolonged; + }, + + onRLSyncError() { + // Like onSyncError, but from the reading-list engine. + // However, the current UX around Sync is that error notifications should + // generally *not* be seen as they typically aren't actionable - so only + // authentication errors (which require user action) and "prolonged" errors + // (which technically aren't actionable, but user really should know anyway) + // are shown. + this.log.debug("onRLSyncError with readingList state", ReadingListScheduler.state); + if (ReadingListScheduler.state == ReadingListScheduler.STATE_ERROR_AUTHENTICATION) { + this.onLoginError(); + return; + } + // If it's not prolonged there's nothing to do. + if (!this.isProlongedReadingListError()) { + this.log.debug("onRLSyncError has a non-authentication, non-prolonged error, so not showing any error UI"); + return; + } + // So it's a prolonged error. + // Unfortunate duplication from below... + this.log.debug("onRLSyncError has a prolonged error"); + let title = this._stringBundle.GetStringFromName("error.sync.title"); + // XXX - this is somewhat wrong - we are reporting the threshold we consider + // to be prolonged, not how long it actually has been. (ie, lastSync below + // is effectively constant) - bit it too is copied from below. + let lastSync = + Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400; + let description = + this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1); + let priority = Weave.Notifications.PRIORITY_INFO; + let buttons = [ + new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"), + this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"), + function() { gSyncUI.doSync(); return true; } + ), + ]; + let notification = + new Weave.Notification(title, description, null, priority, buttons); + Weave.Notifications.replaceTitle(notification); + + this.updateUI(); + }, + onSyncError: function SUI_onSyncError() { + this.log.debug("onSyncError"); let title = this._stringBundle.GetStringFromName("error.sync.title"); if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) { @@ -418,7 +535,9 @@ let gSyncUI = { let priority = Weave.Notifications.PRIORITY_WARNING; let buttons = []; - // Check if the client is outdated in some way + // Check if the client is outdated in some way (but note: we've never in the + // past, and probably never will, bump the relevent version numbers, so + // this is effectively dead code!) let outdated = Weave.Status.sync == Weave.VERSION_OUT_OF_DATE; for (let [engine, reason] in Iterator(Weave.Status.engines)) outdated = outdated || reason == Weave.VERSION_OUT_OF_DATE; @@ -468,6 +587,7 @@ let gSyncUI = { }, observe: function SUI_observe(subject, topic, data) { + this.log.debug("observed", topic); if (this._unloaded) { Cu.reportError("SyncUI observer called after unload: " + topic); return; @@ -480,10 +600,26 @@ let gSyncUI = { subject = subject.wrappedJSObject.object; } + // First handle "activity" only. switch (topic) { case "weave:service:sync:start": + case "weave:service:login:start": + case "readinglist:sync:start": this.onActivityStart(); break; + case "weave:service:sync:finish": + case "weave:service:sync:error": + case "weave:service:login:finish": + case "weave:service:login:error": + case "readinglist:sync:finish": + case "readinglist:sync:error": + this.onActivityStop(); + break; + } + // Now non-activity state (eg, enabled, errors, etc) + // Note that sync uses the ":ui:" notifications for errors because sync. + // ReadingList has no such concept (yet?; hopefully the :error is enough!) + switch (topic) { case "weave:ui:sync:finish": this.onSyncFinish(); break; @@ -496,9 +632,6 @@ let gSyncUI = { case "weave:service:setup-complete": this.onSetupComplete(); break; - case "weave:service:login:start": - this.onActivityStart(); - break; case "weave:service:login:finish": this.onLoginFinish(); break; @@ -523,6 +656,13 @@ let gSyncUI = { case "weave:ui:clear-error": this.clearError(); break; + + case "readinglist:sync:error": + this.onRLSyncError(); + break; + case "readinglist:sync:finish": + this.clearError(); + break; } }, @@ -540,3 +680,6 @@ XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() { createBundle("chrome://weave/locale/services/sync.properties"); }); +XPCOMUtils.defineLazyGetter(gSyncUI, "log", function() { + return Log.repository.getLogger("browserwindow.syncui"); +}); diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 342220ed91b2..8ee8f137d44c 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -37,6 +37,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch", "resource:///modules/ContentSearch.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AboutHome", "resource:///modules/AboutHome.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Log", + "resource://gre/modules/Log.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "Favicons", "@mozilla.org/browser/favicon-service;1", "mozIAsyncFavicons"); diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index 3e818176eb3b..f99cd0303bfc 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -403,6 +403,8 @@ support-files = [browser_ssl_error_reports.js] [browser_star_hsts.js] [browser_subframe_favicons_not_used.js] +[browser_syncui.js] +skip-if = e10s # Bug 1137087 - browser_tabopen_reflows.js fails if this was previously run with e10s [browser_tabDrop.js] skip-if = buildapp == 'mulet' || e10s [browser_tabMatchesInAwesomebar.js] diff --git a/browser/base/content/test/general/browser_syncui.js b/browser/base/content/test/general/browser_syncui.js new file mode 100644 index 000000000000..68f11243260e --- /dev/null +++ b/browser/base/content/test/general/browser_syncui.js @@ -0,0 +1,244 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let {Log} = Cu.import("resource://gre/modules/Log.jsm", {}); +let {Weave} = Cu.import("resource://services-sync/main.js", {}); +let {Notifications} = Cu.import("resource://services-sync/notifications.js", {}); +// The BackStagePass allows us to get this test-only non-exported function. +let {getInternalScheduler} = Cu.import("resource:///modules/readinglist/Scheduler.jsm", {}); + +let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"] + .getService(Ci.nsIStringBundleService) + .createBundle("chrome://weave/locale/services/sync.properties"); + +// ensure test output sees log messages. +Log.repository.getLogger("browserwindow.syncui").addAppender(new Log.DumpAppender()); + +function promiseObserver(topic) { + return new Promise(resolve => { + let obs = (subject, topic, data) => { + Services.obs.removeObserver(obs, topic); + resolve(subject); + } + Services.obs.addObserver(obs, topic, false); + }); +} + +add_task(function* prepare() { + let xps = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + yield xps.whenLoaded(); + // mock out the "_needsSetup()" function so we don't short-circuit. + let oldNeedsSetup = window.gSyncUI._needsSetup; + window.gSyncUI._needsSetup = () => false; + registerCleanupFunction(() => { + window.gSyncUI._needsSetup = oldNeedsSetup; + }); +}); + +add_task(function* testProlongedSyncError() { + let promiseNotificationAdded = promiseObserver("weave:notification:added"); + Assert.equal(Notifications.notifications.length, 0, "start with no notifications"); + + // Pretend we are in the "prolonged error" state. + Weave.Status.sync = Weave.PROLONGED_SYNC_FAILURE; + Weave.Status.login = Weave.LOGIN_SUCCEEDED; + Services.obs.notifyObservers(null, "weave:ui:sync:error", null); + + let subject = yield promiseNotificationAdded; + let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract! + Assert.equal(notification.title, stringBundle.GetStringFromName("error.sync.title")); + Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification"); + + // Now pretend we just had a successful sync - the error notification should go away. + let promiseNotificationRemoved = promiseObserver("weave:notification:removed"); + Weave.Status.sync = Weave.STATUS_OK; + Services.obs.notifyObservers(null, "weave:ui:sync:finish", null); + yield promiseNotificationRemoved; + Assert.equal(Notifications.notifications.length, 0, "no notifications left"); +}); + +add_task(function* testProlongedRLError() { + let promiseNotificationAdded = promiseObserver("weave:notification:added"); + Assert.equal(Notifications.notifications.length, 0, "start with no notifications"); + + // Pretend the reading-list is in the "prolonged error" state. + let longAgo = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000); // 100 days ago. + Services.prefs.setCharPref("readinglist.scheduler.lastSync", longAgo.toString()); + getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_OTHER; + Services.obs.notifyObservers(null, "readinglist:sync:start", null); + Services.obs.notifyObservers(null, "readinglist:sync:error", null); + + let subject = yield promiseNotificationAdded; + let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract! + Assert.equal(notification.title, stringBundle.GetStringFromName("error.sync.title")); + Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification"); + + // Now pretend we just had a successful sync - the error notification should go away. + let promiseNotificationRemoved = promiseObserver("weave:notification:removed"); + Services.prefs.setCharPref("readinglist.scheduler.lastSync", Date.now().toString()); + Services.obs.notifyObservers(null, "readinglist:sync:start", null); + Services.obs.notifyObservers(null, "readinglist:sync:finish", null); + yield promiseNotificationRemoved; + Assert.equal(Notifications.notifications.length, 0, "no notifications left"); +}); + +add_task(function* testSyncLoginError() { + let promiseNotificationAdded = promiseObserver("weave:notification:added"); + Assert.equal(Notifications.notifications.length, 0, "start with no notifications"); + + // Pretend we are in the "prolonged error" state. + Weave.Status.sync = Weave.LOGIN_FAILED; + Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED; + Services.obs.notifyObservers(null, "weave:ui:sync:error", null); + + let subject = yield promiseNotificationAdded; + let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract! + Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title")); + Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification"); + + // Now pretend we just had a successful login - the error notification should go away. + Weave.Status.sync = Weave.STATUS_OK; + Weave.Status.login = Weave.LOGIN_SUCCEEDED; + let promiseNotificationRemoved = promiseObserver("weave:notification:removed"); + Services.obs.notifyObservers(null, "weave:service:login:start", null); + Services.obs.notifyObservers(null, "weave:service:login:finish", null); + yield promiseNotificationRemoved; + Assert.equal(Notifications.notifications.length, 0, "no notifications left"); +}); + +add_task(function* testRLLoginError() { + let promiseNotificationAdded = promiseObserver("weave:notification:added"); + Assert.equal(Notifications.notifications.length, 0, "start with no notifications"); + + // Pretend RL is in an auth error state + getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_AUTHENTICATION; + Services.obs.notifyObservers(null, "readinglist:sync:start", null); + Services.obs.notifyObservers(null, "readinglist:sync:error", null); + + let subject = yield promiseNotificationAdded; + let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract! + Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title")); + Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification"); + + // Now pretend we just had a successful sync - the error notification should go away. + getInternalScheduler().state = ReadingListScheduler.STATE_OK; + let promiseNotificationRemoved = promiseObserver("weave:notification:removed"); + Services.obs.notifyObservers(null, "readinglist:sync:start", null); + Services.obs.notifyObservers(null, "readinglist:sync:finish", null); + yield promiseNotificationRemoved; + Assert.equal(Notifications.notifications.length, 0, "no notifications left"); +}); + +// Here we put readinglist into an "authentication error" state (should see +// the error bar reflecting this), then report a prolonged error from Sync (an +// infobar to reflect the sync error should replace it), then resolve the sync +// error - the authentication error from readinglist should remain. +add_task(function* testRLLoginErrorRemains() { + let promiseNotificationAdded = promiseObserver("weave:notification:added"); + Assert.equal(Notifications.notifications.length, 0, "start with no notifications"); + + // Pretend RL is in an auth error state + getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_AUTHENTICATION; + Services.obs.notifyObservers(null, "readinglist:sync:start", null); + Services.obs.notifyObservers(null, "readinglist:sync:error", null); + + let subject = yield promiseNotificationAdded; + let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract! + Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title")); + Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification"); + + // Now Sync into a prolonged auth error state. + promiseNotificationAdded = promiseObserver("weave:notification:added"); + Weave.Status.sync = Weave.PROLONGED_SYNC_FAILURE; + Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED; + Services.obs.notifyObservers(null, "weave:ui:sync:error", null); + subject = yield promiseNotificationAdded; + // still exactly 1 notification with the "login" title. + notification = subject.wrappedJSObject.object; + Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title")); + Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification"); + + // Resolve the sync problem. + promiseNotificationAdded = promiseObserver("weave:notification:added"); + Weave.Status.sync = Weave.STATUS_OK; + Weave.Status.login = Weave.LOGIN_SUCCEEDED; + Services.obs.notifyObservers(null, "weave:ui:sync:finish", null); + + // Expect one notification - the RL login problem. + subject = yield promiseNotificationAdded; + // still exactly 1 notification with the "login" title. + notification = subject.wrappedJSObject.object; + Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title")); + Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification"); + + // and cleanup - resolve the readinglist error. + getInternalScheduler().state = ReadingListScheduler.STATE_OK; + let promiseNotificationRemoved = promiseObserver("weave:notification:removed"); + Services.obs.notifyObservers(null, "readinglist:sync:start", null); + Services.obs.notifyObservers(null, "readinglist:sync:finish", null); + yield promiseNotificationRemoved; + Assert.equal(Notifications.notifications.length, 0, "no notifications left"); +}); + +function checkButtonsStatus(shouldBeActive) { + let button = document.getElementById("sync-button"); + let panelbutton = document.getElementById("PanelUI-fxa-status"); + if (shouldBeActive) { + Assert.equal(button.getAttribute("status"), "active"); + Assert.equal(panelbutton.getAttribute("syncstatus"), "active"); + } else { + Assert.ok(!button.hasAttribute("status")); + Assert.ok(!panelbutton.hasAttribute("syncstatus")); + } +} + +function testButtonActions(startNotification, endNotification) { + checkButtonsStatus(false); + // pretend a sync is starting. + Services.obs.notifyObservers(null, startNotification, null); + checkButtonsStatus(true); + // and has stopped + Services.obs.notifyObservers(null, endNotification, null); + checkButtonsStatus(false); +} + +add_task(function* testButtonActivities() { + // add the Sync button to the panel so we can get it! + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + // check the button's functionality + yield PanelUI.show(); + try { + testButtonActions("weave:service:login:start", "weave:service:login:finish"); + testButtonActions("weave:service:login:start", "weave:service:login:error"); + + testButtonActions("weave:service:sync:start", "weave:service:sync:finish"); + testButtonActions("weave:service:sync:start", "weave:service:sync:error"); + + testButtonActions("readinglist:sync:start", "readinglist:sync:finish"); + testButtonActions("readinglist:sync:start", "readinglist:sync:error"); + + // and ensure the counters correctly handle multiple in-flight syncs + Services.obs.notifyObservers(null, "weave:service:sync:start", null); + checkButtonsStatus(true); + Services.obs.notifyObservers(null, "readinglist:sync:start", null); + checkButtonsStatus(true); + Services.obs.notifyObservers(null, "readinglist:sync:finish", null); + // sync is still going... + checkButtonsStatus(true); + // another reading list starts + Services.obs.notifyObservers(null, "readinglist:sync:start", null); + checkButtonsStatus(true); + // The initial sync stops. + Services.obs.notifyObservers(null, "weave:service:sync:finish", null); + // RL is still going... + checkButtonsStatus(true); + // RL finishes with an error, so no longer active. + Services.obs.notifyObservers(null, "readinglist:sync:error", null); + checkButtonsStatus(false); + } finally { + PanelUI.hide(); + CustomizableUI.removeWidgetFromArea("sync-button"); + } +}); diff --git a/browser/components/downloads/content/allDownloadsViewOverlay.js b/browser/components/downloads/content/allDownloadsViewOverlay.js index 05bd2890005a..c0dd090d2e23 100644 --- a/browser/components/downloads/content/allDownloadsViewOverlay.js +++ b/browser/components/downloads/content/allDownloadsViewOverlay.js @@ -4,6 +4,8 @@ let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", diff --git a/browser/components/downloads/content/downloads.js b/browser/components/downloads/content/downloads.js index 3391a51f2fc3..5754b10b6a7c 100755 --- a/browser/components/downloads/content/downloads.js +++ b/browser/components/downloads/content/downloads.js @@ -66,6 +66,8 @@ let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", "resource:///modules/DownloadsCommon.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI", diff --git a/browser/components/readinglist/Scheduler.jsm b/browser/components/readinglist/Scheduler.jsm new file mode 100644 index 000000000000..8acfc13dbabe --- /dev/null +++ b/browser/components/readinglist/Scheduler.jsm @@ -0,0 +1,338 @@ +/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + + +XPCOMUtils.defineLazyModuleGetter(this, 'LogManager', + 'resource://services-common/logmanager.js'); + +XPCOMUtils.defineLazyModuleGetter(this, 'Log', + 'resource://gre/modules/Log.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, 'Preferences', + 'resource://gre/modules/Preferences.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout', + 'resource://gre/modules/Timer.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'clearTimeout', + 'resource://gre/modules/Timer.jsm'); + +Cu.import('resource://gre/modules/Task.jsm'); + +this.EXPORTED_SYMBOLS = ["ReadingListScheduler"]; + +// A list of "external" observer topics that may cause us to change when we +// sync. +const OBSERVERS = [ + // We don't sync when offline and restart when online. + "network:offline-status-changed", + // FxA notifications also cause us to check if we should sync. + "fxaccounts:onverified", + // When something notices a local change to an item. + "readinglist:item-changed", + // some notifications the engine might send if we have been requested to backoff. + "readinglist:backoff-requested", + // request to sync now + "readinglist:user-sync", + +]; + +///////// A temp object until we get our "engine" +let engine = { + ERROR_AUTHENTICATION: "authentication error", + sync: Task.async(function* () { + }), +} + +let prefs = new Preferences("readinglist.scheduler."); + +// A helper to manage our interval values. +let intervals = { + // Getters for our intervals. + _fixupIntervalPref(prefName, def) { + // All pref values are seconds, but we return ms. + return prefs.get(prefName, def) * 1000; + }, + + // How long after startup do we do an initial sync? + get initial() this._fixupIntervalPref("initial", 20), // 20 seconds. + // Every interval after the first. + get schedule() this._fixupIntervalPref("schedule", 2 * 60 * 60), // 2 hours + // After we've been told an item has changed + get dirty() this._fixupIntervalPref("dirty", 2 * 60), // 2 mins + // After an error + get retry() this._fixupIntervalPref("retry", 2 * 60), // 2 mins +}; + +// This is the implementation, but it's not exposed directly. +function InternalScheduler() { + // oh, I don't know what logs yet - let's guess! + let logs = ["readinglist", "FirefoxAccounts", "browserwindow.syncui"]; + this._logManager = new LogManager("readinglist.", logs, "readinglist"); + this.log = Log.repository.getLogger("readinglist.scheduler"); + this.log.info("readinglist scheduler created.") + this.state = this.STATE_OK; + + // don't this.init() here, but instead at the module level - tests want to + // add hooks before it is called. +} + +InternalScheduler.prototype = { + // When the next scheduled sync should happen. If we can sync, there will + // be a timer set to fire then. If we can't sync there will not be a timer, + // but it will be set to fire then as soon as we can. + _nextScheduledSync: null, + // The time when the most-recent "backoff request" expires - we will never + // schedule a new timer before this. + _backoffUntil: 0, + // Our current timer. + _timer: null, + // Our timer fires a promise - _timerRunning is true until it resolves or + // rejects. + _timerRunning: false, + // Our sync engine - XXX - maybe just a callback? + _engine: engine, + + // Our state variable and constants. + state: null, + STATE_OK: "ok", + STATE_ERROR_AUTHENTICATION: "authentication error", + STATE_ERROR_OTHER: "other error", + + init() { + this.log.info("scheduler initialzing"); + this._observe = this.observe.bind(this); + for (let notification of OBSERVERS) { + Services.obs.addObserver(this._observe, notification, false); + } + this._nextScheduledSync = Date.now() + intervals.initial; + this._setupTimer(); + }, + + // Note: only called by tests. + finalize() { + this.log.info("scheduler finalizing"); + this._clearTimer(); + for (let notification of OBSERVERS) { + Services.obs.removeObserver(this._observe, notification); + } + this._observe = null; + }, + + observe(subject, topic, data) { + this.log.debug("observed ${}", topic); + switch (topic) { + case "readinglist:backoff-requested": { + // The subject comes in as a string, a number of seconds. + let interval = parseInt(data, 10); + if (isNaN(interval)) { + this.log.warn("Backoff request had non-numeric value", data); + return; + } + this.log.info("Received a request to backoff for ${} seconds", interval); + this._backoffUntil = Date.now() + interval * 1000; + this._maybeReschedule(0); + break; + } + case "readinglist:local:dirty": + this._maybeReschedule(intervals.dirty); + break; + case "readinglist:user-sync": + this._syncNow(); + break; + case "fxaccounts:onverified": + // If we were in an authentication error state, reset that now. + if (this.state == this.STATE_ERROR_AUTHENTICATION) { + this.state = this.STATE_OK; + } + break; + + // The rest just indicate that now is probably a good time to check if + // we can sync as normal using whatever schedule was previously set. + default: + break; + } + // When observers fire we ignore the current sync error state as the + // notification may indicate it's been resolved. + this._setupTimer(true); + }, + + // Is the current error state such that we shouldn't schedule a new sync. + _isBlockedOnError() { + // this needs more thought... + return this.state == this.STATE_ERROR_AUTHENTICATION; + }, + + // canSync indicates if we can currently sync. + _canSync(ignoreBlockingErrors = false) { + if (Services.io.offline) { + this.log.info("canSync=false - we are offline"); + return false; + } + if (!ignoreBlockingErrors && this._isBlockedOnError()) { + this.log.info("canSync=false - we are in a blocked error state", this.state); + return false; + } + this.log.info("canSync=true"); + return true; + }, + + // _setupTimer checks the current state and the environment to see when + // we should next sync and creates the timer with the appropriate delay. + _setupTimer(ignoreBlockingErrors = false) { + if (!this._canSync(ignoreBlockingErrors)) { + this._clearTimer(); + return; + } + if (this._timer) { + let when = new Date(this._nextScheduledSync); + let delay = this._nextScheduledSync - Date.now(); + this.log.info("checkStatus - already have a timer - will fire in ${delay}ms at ${when}", + {delay, when}); + return; + } + if (this._timerRunning) { + this.log.info("checkStatus - currently syncing"); + return; + } + // no timer and we can sync, so start a new one. + let now = Date.now(); + let delay = Math.max(0, this._nextScheduledSync - now); + let when = new Date(now + delay); + this.log.info("next scheduled sync is in ${delay}ms (at ${when})", {delay, when}) + this._timer = this._setTimeout(delay); + }, + + // Something (possibly naively) thinks the next sync should happen in + // delay-ms. If there's a backoff in progress, ignore the requested delay + // and use the back-off. If there's already a timer scheduled for earlier + // than delay, let the earlier timer remain. Otherwise, use the requested + // delay. + _maybeReschedule(delay) { + // If there's no delay specified and there's nothing currently scheduled, + // it means a backoff request while the sync is actually running - there's + // no need to do anything here - the next reschedule after the sync + // completes will take the backoff into account. + if (!delay && !this._nextScheduledSync) { + this.log.debug("_maybeReschedule ignoring a backoff request while running"); + return; + } + let now = Date.now(); + if (!this._nextScheduledSync) { + this._nextScheduledSync = now + delay; + } + // If there is something currently scheduled before the requested delay, + // keep the existing value (eg, if we have a timer firing in 1 second, and + // get a "dirty" notification that says we should sync in 2 seconds, we + // keep the 1 second value) + this._nextScheduledSync = Math.min(this._nextScheduledSync, now + delay); + // But we still need to honor a backoff. + this._nextScheduledSync = Math.max(this._nextScheduledSync, this._backoffUntil); + // And always create a new timer next time _setupTimer is called. + this._clearTimer(); + }, + + // callback for when the timer fires. + _doSync() { + this.log.debug("starting sync"); + this._timer = null; + this._timerRunning = true; + // flag that there's no new schedule yet, so a request coming in while + // we are running does the right thing. + this._nextScheduledSync = 0; + Services.obs.notifyObservers(null, "readinglist:sync:start", null); + this._engine.sync().then(() => { + this.log.info("Sync completed successfully"); + // Write a pref in the same format used to services/sync to indicate + // the last success. + prefs.set("lastSync", new Date().toString()); + this.state = this.STATE_OK; + this._logManager.resetFileLog(this._logManager.REASON_SUCCESS); + Services.obs.notifyObservers(null, "readinglist:sync:finish", null); + return intervals.schedule; + }).catch(err => { + this.log.error("Sync failed", err); + // XXX - how to detect an auth error? + this.state = err == this._engine.ERROR_AUTHENTICATION ? + this.STATE_ERROR_AUTHENTICATION : this.STATE_ERROR_OTHER; + this._logManager.resetFileLog(this._logManager.REASON_ERROR); + Services.obs.notifyObservers(null, "readinglist:sync:error", null); + return intervals.retry; + }).then(nextDelay => { + this._timerRunning = false; + // ensure a new timer is setup for the appropriate next time. + this._maybeReschedule(nextDelay); + this._setupTimer(); + this._onAutoReschedule(); // just for tests... + }).catch(err => { + // We should never get here, but better safe than sorry... + this.log.error("Failed to reschedule after sync completed", err); + }); + }, + + _clearTimer() { + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + } + }, + + // A function to "sync now", but not allowing it to start if one is + // already running, and rescheduling the timer. + // To call this, just send a "readinglist:user-sync" notification. + _syncNow() { + if (this._timerRunning) { + this.log.info("syncNow() but a sync is already in progress - ignoring"); + return; + } + this._clearTimer(); + this._doSync(); + }, + + // A couple of hook-points for testing. + // xpcshell tests hook this so (a) it can check the expected delay is set + // and (b) to ignore the delay and set a timeout of 0 so the test is fast. + _setTimeout(delay) { + return setTimeout(() => this._doSync(), delay); + }, + // xpcshell tests hook this to make sure that the correct state etc exist + // after a sync has been completed and a new timer created (or not). + _onAutoReschedule() {}, +}; + +let internalScheduler = new InternalScheduler(); +internalScheduler.init(); + +// The public interface into this module is tiny, so a simple object that +// delegates to the implementation. +let ReadingListScheduler = { + get STATE_OK() internalScheduler.STATE_OK, + get STATE_ERROR_AUTHENTICATION() internalScheduler.STATE_ERROR_AUTHENTICATION, + get STATE_ERROR_OTHER() internalScheduler.STATE_ERROR_OTHER, + + get state() internalScheduler.state, +}; + +// These functions are exposed purely for tests, which manage to grab them +// via a BackstagePass. +function createTestableScheduler() { + // kill the "real" scheduler as we don't want it listening to notifications etc. + if (internalScheduler) { + internalScheduler.finalize(); + internalScheduler = null; + } + // No .init() call - that's up to the tests after hooking. + return new InternalScheduler(); +} + +// mochitests want the internal state of the real scheduler for various things. +function getInternalScheduler() { + return internalScheduler; +} diff --git a/browser/components/readinglist/moz.build b/browser/components/readinglist/moz.build index 0dfdf141706c..b02a45bf7d72 100644 --- a/browser/components/readinglist/moz.build +++ b/browser/components/readinglist/moz.build @@ -13,3 +13,9 @@ TESTING_JS_MODULES += [ ] BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini'] + +XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini'] + +EXTRA_JS_MODULES.readinglist += [ + 'Scheduler.jsm', +] diff --git a/browser/components/readinglist/test/xpcshell/head.js b/browser/components/readinglist/test/xpcshell/head.js new file mode 100644 index 000000000000..caf9f95a9552 --- /dev/null +++ b/browser/components/readinglist/test/xpcshell/head.js @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); diff --git a/browser/components/readinglist/test/xpcshell/test_scheduler.js b/browser/components/readinglist/test/xpcshell/test_scheduler.js new file mode 100644 index 000000000000..bb258a4720d2 --- /dev/null +++ b/browser/components/readinglist/test/xpcshell/test_scheduler.js @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout', + 'resource://gre/modules/Timer.jsm'); + +// Setup logging prefs before importing the scheduler module. +Services.prefs.setCharPref("readinglist.log.appender.dump", "Trace"); + +let {createTestableScheduler} = Cu.import("resource:///modules/readinglist/Scheduler.jsm", {}); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); + +// Log rotation needs a profile dir. +do_get_profile(); + +let prefs = new Preferences("readinglist.scheduler."); + +function promiseObserver(topic) { + return new Promise(resolve => { + let obs = (subject, topic, data) => { + Services.obs.removeObserver(obs, topic); + resolve(data); + } + Services.obs.addObserver(obs, topic, false); + }); +} + +function createScheduler(options) { + // avoid typos in the test and other footguns in the options. + let allowedOptions = ["expectedDelay", "expectNewTimer", "syncFunction"]; + for (let key of Object.keys(options)) { + if (!allowedOptions.includes(key)) { + throw new Error("Invalid option " + key); + } + } + let scheduler = createTestableScheduler(); + // make our hooks + let syncFunction = options.syncFunction || Promise.resolve; + scheduler._engine.sync = syncFunction; + // we expect _setTimeout to be called *twice* - first is the initial sync, + // and there's no need to test the delay used for that. options.expectedDelay + // is to check the *subsequent* timer. + let numCalls = 0; + scheduler._setTimeout = function(delay) { + ++numCalls; + print("Test scheduler _setTimeout call number " + numCalls + " with delay=" + delay); + switch (numCalls) { + case 1: + // this is the first and boring schedule as it initializes - do nothing + // other than return a timer that fires immediately. + return setTimeout(() => scheduler._doSync(), 0); + break; + case 2: + // This is the one we are interested in, so check things. + if (options.expectedDelay) { + // a little slop is OK as it takes a few ms to actually set the timer + ok(Math.abs(options.expectedDelay * 1000 - delay) < 500, [options.expectedDelay * 1000, delay]); + } + // and return a timeout that "never" fires + return setTimeout(() => scheduler._doSync(), 10000000); + break; + default: + // This is unexpected! + ok(false, numCalls); + } + }; + // And a callback made once we've determined the next delay. This is always + // called even if _setTimeout isn't (due to no timer being created) + scheduler._onAutoReschedule = () => { + // Most tests expect a new timer, so this is "opt out" + let expectNewTimer = options.expectNewTimer === undefined ? true : options.expectNewTimer; + ok(expectNewTimer ? scheduler._timer : !scheduler._timer); + } + // calling .init fires things off... + scheduler.init(); + return scheduler; +} + +add_task(function* testSuccess() { + // promises which resolve once we've got all the expected notifications. + let allNotifications = [ + promiseObserver("readinglist:sync:start"), + promiseObserver("readinglist:sync:finish"), + ]; + // New delay should be "as regularly scheduled". + prefs.set("schedule", 100); + let scheduler = createScheduler({expectedDelay: 100}); + yield Promise.all(allNotifications); + scheduler.finalize(); +}); + +add_task(function* testOffline() { + let scheduler = createScheduler({expectNewTimer: false}); + Services.io.offline = true; + ok(!scheduler._canSync(), "_canSync is false when offline.") + ok(!scheduler._timer, "there is no current timer while offline.") + Services.io.offline = false; + ok(scheduler._canSync(), "_canSync is true when online.") + ok(scheduler._timer, "there is a new timer when back online.") + scheduler.finalize(); +}); + +add_task(function* testRetryableError() { + let allNotifications = [ + promiseObserver("readinglist:sync:start"), + promiseObserver("readinglist:sync:error"), + ]; + prefs.set("retry", 10); + let scheduler = createScheduler({ + expectedDelay: 10, + syncFunction: () => Promise.reject("transient"), + }); + yield Promise.all(allNotifications); + scheduler.finalize(); +}); + +add_task(function* testAuthError() { + prefs.set("retry", 10); + // We expect an auth error to result in no new timer (as it's waiting for + // some indication it can proceed), but with the next delay being a normal + // "retry" interval (so when we can proceed it is probably already stale, so + // is effectively "immediate") + let scheduler = createScheduler({ + expectedDelay: 10, + syncFunction: () => { + return Promise.reject(ReadingListScheduler._engine.ERROR_AUTHENTICATION); + }, + expectNewTimer: false + }); + // XXX - TODO - send an observer that "unblocks" us and ensure we actually + // do unblock. + scheduler.finalize(); +}); + +add_task(function* testBackoff() { + let scheduler = createScheduler({expectedDelay: 1000}); + Services.obs.notifyObservers(null, "readinglist:backoff-requested", 1000); + // XXX - this needs a little love as nothing checks createScheduler actually + // made the checks we think it does. + scheduler.finalize(); +}); + +function run_test() { + run_next_test(); +} diff --git a/browser/components/readinglist/test/xpcshell/xpcshell.ini b/browser/components/readinglist/test/xpcshell/xpcshell.ini new file mode 100644 index 000000000000..e204dde472e9 --- /dev/null +++ b/browser/components/readinglist/test/xpcshell/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +head = head.js +firefox-appdir = browser + +[test_scheduler.js] diff --git a/browser/components/search/test/browser_searchbar_openpopup.js b/browser/components/search/test/browser_searchbar_openpopup.js index 5efa7da2635f..48a3308c550a 100644 --- a/browser/components/search/test/browser_searchbar_openpopup.js +++ b/browser/components/search/test/browser_searchbar_openpopup.js @@ -452,6 +452,28 @@ add_task(function* dont_rollup_oncaretmove() { is(textbox.selectionEnd, 9, "Should have moved the caret (selectionEnd after right)"); is(searchPopup.state, "open", "Popup should still be open"); + // Ensure caret movement works while a suggestion is selected. + is(textbox.popup.selectedIndex, -1, "No selected item in list"); + EventUtils.synthesizeKey("VK_DOWN", {}); + is(textbox.popup.selectedIndex, 0, "Selected item in list"); + is(textbox.selectionStart, 9, "Should have moved the caret to the end (selectionStart after selection)"); + is(textbox.selectionEnd, 9, "Should have moved the caret to the end (selectionEnd after selection)"); + + EventUtils.synthesizeKey("VK_LEFT", {}); + is(textbox.selectionStart, 8, "Should have moved the caret again (selectionStart after left)"); + is(textbox.selectionEnd, 8, "Should have moved the caret again (selectionEnd after left)"); + is(searchPopup.state, "open", "Popup should still be open"); + + EventUtils.synthesizeKey("VK_LEFT", {}); + is(textbox.selectionStart, 7, "Should have moved the caret (selectionStart after left)"); + is(textbox.selectionEnd, 7, "Should have moved the caret (selectionEnd after left)"); + is(searchPopup.state, "open", "Popup should still be open"); + + EventUtils.synthesizeKey("VK_RIGHT", {}); + is(textbox.selectionStart, 8, "Should have moved the caret (selectionStart after right)"); + is(textbox.selectionEnd, 8, "Should have moved the caret (selectionEnd after right)"); + is(searchPopup.state, "open", "Popup should still be open"); + if (navigator.platform.indexOf("Mac") == -1) { EventUtils.synthesizeKey("VK_HOME", {}); is(textbox.selectionStart, 0, "Should have moved the caret (selectionStart after home)"); diff --git a/browser/devtools/netmonitor/test/browser_net_content-type.js b/browser/devtools/netmonitor/test/browser_net_content-type.js index 6dd94a000c26..800dff7a3eb5 100644 --- a/browser/devtools/netmonitor/test/browser_net_content-type.js +++ b/browser/devtools/netmonitor/test/browser_net_content-type.js @@ -48,7 +48,7 @@ function test() { statusText: "OK", type: "json", fullMimeType: "application/json; charset=utf-8", - size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03), time: true }); verifyRequestItemTarget(RequestsMenu.getItemAtIndex(4), @@ -67,7 +67,7 @@ function test() { statusText: "OK", type: "png", fullMimeType: "image/png", - size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.75), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.76), time: true }); diff --git a/browser/devtools/netmonitor/test/browser_net_sort-02.js b/browser/devtools/netmonitor/test/browser_net_sort-02.js index 7a6becf68e85..7a5eb7e0c9ae 100644 --- a/browser/devtools/netmonitor/test/browser_net_sort-02.js +++ b/browser/devtools/netmonitor/test/browser_net_sort-02.js @@ -228,8 +228,8 @@ function test() { statusText: "Meh", type: "2", fullMimeType: "text/2", - transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01), - size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01), + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02), time: true }); verifyRequestItemTarget(RequestsMenu.getItemAtIndex(c), @@ -239,8 +239,8 @@ function test() { statusText: "Meh", type: "3", fullMimeType: "text/3", - transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02), - size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02), + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03), time: true }); verifyRequestItemTarget(RequestsMenu.getItemAtIndex(d), @@ -250,8 +250,8 @@ function test() { statusText: "Meh", type: "4", fullMimeType: "text/4", - transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03), - size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03), + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04), time: true }); verifyRequestItemTarget(RequestsMenu.getItemAtIndex(e), @@ -261,8 +261,8 @@ function test() { statusText: "Meh", type: "5", fullMimeType: "text/5", - transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04), - size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04), + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.05), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.05), time: true }); diff --git a/browser/devtools/netmonitor/test/browser_net_sort-03.js b/browser/devtools/netmonitor/test/browser_net_sort-03.js index d4c1de33b720..8a60142cff0b 100644 --- a/browser/devtools/netmonitor/test/browser_net_sort-03.js +++ b/browser/devtools/netmonitor/test/browser_net_sort-03.js @@ -143,8 +143,8 @@ function test() { statusText: "Meh", type: "2", fullMimeType: "text/2", - transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01), - size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.01), + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02), time: true }); } @@ -156,8 +156,8 @@ function test() { statusText: "Meh", type: "3", fullMimeType: "text/3", - transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02), - size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.02), + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03), time: true }); } @@ -169,8 +169,8 @@ function test() { statusText: "Meh", type: "4", fullMimeType: "text/4", - transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03), - size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.03), + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04), time: true }); } @@ -182,8 +182,8 @@ function test() { statusText: "Meh", type: "5", fullMimeType: "text/5", - transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04), - size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.04), + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.05), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeKB", 0.05), time: true }); } diff --git a/browser/devtools/projecteditor/lib/stores/resource.js b/browser/devtools/projecteditor/lib/stores/resource.js index 60343f0ed33c..d91f86550df4 100644 --- a/browser/devtools/projecteditor/lib/stores/resource.js +++ b/browser/devtools/projecteditor/lib/stores/resource.js @@ -47,11 +47,6 @@ var Resource = Class({ this.uri = uri; }, - /** - * Return the trailing name component of this.uri. - */ - get basename() { return this.uri.path.replace(/\/+$/, '').replace(/\\/g,'/').replace( /.*\//, '' ); }, - /** * Is there more than 1 child Resource? */ @@ -238,6 +233,13 @@ var FileResource = Class({ return this._refreshDeferred.promise; }, + /** + * Return the trailing name component of this Resource + */ + get basename() { + return this.path.replace(/\/+$/, '').replace(/\\/g,'/').replace( /.*\//, '' ); + }, + /** * A string to be used when displaying this Resource in views */ diff --git a/browser/devtools/projecteditor/test/browser_projecteditor_rename_file.js b/browser/devtools/projecteditor/test/browser_projecteditor_rename_file.js index 1292cf694619..06546098a5de 100644 --- a/browser/devtools/projecteditor/test/browser_projecteditor_rename_file.js +++ b/browser/devtools/projecteditor/test/browser_projecteditor_rename_file.js @@ -13,7 +13,29 @@ add_task(function*() { let root = [...projecteditor.project.allStores()][0].root; is(root.path, TEMP_PATH, "The root store is set to the correct temp path."); for (let child of root.children) { - yield renameWithContextMenu(projecteditor, projecteditor.projectTree.getViewContainer(child)); + yield renameWithContextMenu(projecteditor, + projecteditor.projectTree.getViewContainer(child), + ".renamed"); + } +}); + +add_task(function*() { + let projecteditor = yield addProjectEditorTabForTempDirectory(); + ok(true, "ProjectEditor has loaded"); + + let root = [...projecteditor.project.allStores()][0].root; + is(root.path, TEMP_PATH, "The root store is set to the correct temp path."); + + let childrenList = new Array(); + for (let child of root.children) { + yield renameWithContextMenu(projecteditor, + projecteditor.projectTree.getViewContainer(child), + ".ren\u0061\u0308med"); + childrenList.push(child.basename + ".ren\u0061\u0308med"); + } + for (let child of root.children) { + is (childrenList.indexOf(child.basename) == -1, false, + "Failed to update tree with non-ascii character"); } }); @@ -25,7 +47,7 @@ function openContextMenuOn(node) { ); } -function renameWithContextMenu(projecteditor, container) { +function renameWithContextMenu(projecteditor, container, newName) { let defer = promise.defer(); let popup = projecteditor.contextMenuPopup; let resource = container.resource; @@ -39,7 +61,7 @@ function renameWithContextMenu(projecteditor, container) { projecteditor.project.on("refresh-complete", function refreshComplete() { projecteditor.project.off("refresh-complete", refreshComplete); - OS.File.stat(resource.path + ".renamed").then(() => { + OS.File.stat(resource.path + newName).then(() => { ok (true, "File is renamed"); defer.resolve(); }, (ex) => { @@ -50,7 +72,8 @@ function renameWithContextMenu(projecteditor, container) { renameCommand.click(); popup.hidePopup(); - EventUtils.sendString(resource.basename + ".renamed", projecteditor.window); + let input = container.elt.previousElementSibling; + input.value = resource.basename + newName; EventUtils.synthesizeKey("VK_RETURN", {}, projecteditor.window); }); diff --git a/browser/devtools/shared/test/browser_graphs-09a.js b/browser/devtools/shared/test/browser_graphs-09a.js index ff59ce997488..bd5c9d9dc6f8 100644 --- a/browser/devtools/shared/test/browser_graphs-09a.js +++ b/browser/devtools/shared/test/browser_graphs-09a.js @@ -54,7 +54,7 @@ function* testGraph(graph) { is(graph._maxTooltip.querySelector("[text=value]").textContent, "60", "The maximum tooltip displays the correct value."); - is(graph._avgTooltip.querySelector("[text=value]").textContent, "41.71", + is(graph._avgTooltip.querySelector("[text=value]").textContent, "41.72", "The average tooltip displays the correct value."); is(graph._minTooltip.querySelector("[text=value]").textContent, "10", "The minimum tooltip displays the correct value."); diff --git a/browser/devtools/shared/test/browser_num-l10n.js b/browser/devtools/shared/test/browser_num-l10n.js index a7a70abaaab0..a2a986b2e52c 100644 --- a/browser/devtools/shared/test/browser_num-l10n.js +++ b/browser/devtools/shared/test/browser_num-l10n.js @@ -8,7 +8,7 @@ let {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {} function test() { let l10n = new ViewHelpers.L10N(); - is(l10n.numberWithDecimals(1234.56789, 2), "1,234.56", + is(l10n.numberWithDecimals(1234.56789, 2), "1,234.57", "The first number was properly localized."); is(l10n.numberWithDecimals(0.0001, 2), "0", "The second number was properly localized."); diff --git a/browser/devtools/shared/widgets/ViewHelpers.jsm b/browser/devtools/shared/widgets/ViewHelpers.jsm index 5b504fe19a1f..9d19563a6ea3 100644 --- a/browser/devtools/shared/widgets/ViewHelpers.jsm +++ b/browser/devtools/shared/widgets/ViewHelpers.jsm @@ -369,10 +369,6 @@ ViewHelpers.L10N.prototype = { if (isNaN(aNumber) || aNumber == null) { return "0"; } - // Remove {n} trailing decimals. Can't use toFixed(n) because - // toLocaleString converts the number to a string. Also can't use - // toLocaleString(, { maximumFractionDigits: n }) because it's not - // implemented on OS X (bug 368838). Gross. let localized = aNumber.toLocaleString(); // localize // If no grouping or decimal separators are available, bail out, because @@ -381,9 +377,10 @@ ViewHelpers.L10N.prototype = { return localized; } - let padded = localized + new Array(aDecimals).join("0"); // pad with zeros - let match = padded.match("([^]*?\\d{" + aDecimals + "})\\d*$"); - return match.pop(); + return aNumber.toLocaleString(undefined, { + maximumFractionDigits: aDecimals, + minimumFractionDigits: aDecimals + }); } }; diff --git a/browser/devtools/styleeditor/StyleEditorUI.jsm b/browser/devtools/styleeditor/StyleEditorUI.jsm index e9832d3d2628..ad899a16f343 100644 --- a/browser/devtools/styleeditor/StyleEditorUI.jsm +++ b/browser/devtools/styleeditor/StyleEditorUI.jsm @@ -447,13 +447,15 @@ StyleEditorUI.prototype = { * Editor to create UI for. */ _sourceLoaded: function(editor) { + let ordinal = editor.styleSheet.styleSheetIndex; + ordinal = ordinal == -1 ? Number.MAX_SAFE_INTEGER : ordinal; // add new sidebar item and editor to the UI this._view.appendTemplatedItem(STYLE_EDITOR_TEMPLATE, { data: { editor: editor }, disableAnimations: this._alwaysDisableAnimations, - ordinal: editor.styleSheet.styleSheetIndex, + ordinal: ordinal, onCreate: function(summary, details, data) { let editor = data.editor; editor.summary = summary; diff --git a/browser/devtools/styleeditor/StyleSheetEditor.jsm b/browser/devtools/styleeditor/StyleSheetEditor.jsm index 141ea7c606ae..f2d2533f9799 100644 --- a/browser/devtools/styleeditor/StyleSheetEditor.jsm +++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm @@ -15,6 +15,7 @@ const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devt const Editor = require("devtools/sourceeditor/editor"); const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const {CssLogic} = require("devtools/styleinspector/css-logic"); +const {console} = require("resource://gre/modules/devtools/Console.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); @@ -251,11 +252,27 @@ StyleSheetEditor.prototype = { callback(source); } return source; + }, e => { + if (this._isDestroyed) { + console.warn("Could not fetch the source for " + + this.styleSheet.href + + ", the editor was destroyed"); + Cu.reportError(e); + } else { + throw e; + } }); }, e => { - this.emit("error", { key: LOAD_ERROR, append: this.styleSheet.href }); - throw e; - }) + if (this._isDestroyed) { + console.warn("Could not fetch the source for " + + this.styleSheet.href + + ", the editor was destroyed"); + Cu.reportError(e); + } else { + this.emit("error", { key: LOAD_ERROR, append: this.styleSheet.href }); + throw e; + } + }); }, /** @@ -712,6 +729,7 @@ StyleSheetEditor.prototype = { this.cssSheet.off("property-change", this._onPropertyChange); this.cssSheet.off("media-rules-changed", this._onMediaRulesChanged); this.styleSheet.off("error", this._onError); + this._isDestroyed = true; } } diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_autocomplete.js b/browser/devtools/styleeditor/test/browser_styleeditor_autocomplete.js index c7847009661d..650da6d4fc5a 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_autocomplete.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_autocomplete.js @@ -9,7 +9,7 @@ // thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); -const TESTCASE_URI = TEST_BASE + "autocomplete.html"; +const TESTCASE_URI = TEST_BASE_HTTP + "autocomplete.html"; const MAX_SUGGESTIONS = 15; // Pref which decides if CSS autocompletion is enabled in Style Editor or not. diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js b/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js index d51773e70869..5f346810b68d 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js @@ -8,7 +8,7 @@ // thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); -const TESTCASE_URI = TEST_BASE + "four.html"; +const TESTCASE_URI = TEST_BASE_HTTP + "four.html"; let gUI; diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_filesave.js b/browser/devtools/styleeditor/test/browser_styleeditor_filesave.js index 990d2900503c..1ef4e545c90c 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_filesave.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_filesave.js @@ -2,8 +2,8 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -const TESTCASE_URI_HTML = TEST_BASE + "simple.html"; -const TESTCASE_URI_CSS = TEST_BASE + "simple.css"; +const TESTCASE_URI_HTML = TEST_BASE_HTTP + "simple.html"; +const TESTCASE_URI_CSS = TEST_BASE_HTTP + "simple.css"; const Cc = Components.classes; const Ci = Components.interfaces; diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_init.js b/browser/devtools/styleeditor/test/browser_styleeditor_init.js index dd35502150d4..1d1b004e2c32 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_init.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_init.js @@ -9,7 +9,7 @@ // thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: summary is undefined"); -const TESTCASE_URI = TEST_BASE + "simple.html"; +const TESTCASE_URI = TEST_BASE_HTTP + "simple.html"; let gUI; diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_inline_friendly_names.js b/browser/devtools/styleeditor/test/browser_styleeditor_inline_friendly_names.js index 9411b639f29d..55e0a43b4e4d 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_inline_friendly_names.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_inline_friendly_names.js @@ -11,8 +11,8 @@ thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); let gUI; -const FIRST_TEST_PAGE = TEST_BASE + "inline-1.html" -const SECOND_TEST_PAGE = TEST_BASE + "inline-2.html" +const FIRST_TEST_PAGE = TEST_BASE_HTTP + "inline-1.html" +const SECOND_TEST_PAGE = TEST_BASE_HTTP + "inline-2.html" const SAVE_PATH = "test.css"; function test() diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_new.js b/browser/devtools/styleeditor/test/browser_styleeditor_new.js index 2ecd025f73ad..07127e4c9c5c 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_new.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_new.js @@ -9,7 +9,7 @@ // thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); -const TESTCASE_URI = TEST_BASE + "simple.html"; +const TESTCASE_URI = TEST_BASE_HTTP + "simple.html"; let TESTCASE_CSS_SOURCE = "body{background-color:red;"; diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_nostyle.js b/browser/devtools/styleeditor/test/browser_styleeditor_nostyle.js index bc4263f88551..843dfba75c70 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_nostyle.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_nostyle.js @@ -2,7 +2,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -const TESTCASE_URI = TEST_BASE + "nostyle.html"; +const TESTCASE_URI = TEST_BASE_HTTP + "nostyle.html"; function test() diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_pretty.js b/browser/devtools/styleeditor/test/browser_styleeditor_pretty.js index 1ed771b6484a..f158a85e31f2 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_pretty.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_pretty.js @@ -9,7 +9,7 @@ // thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); -const TESTCASE_URI = TEST_BASE + "minified.html"; +const TESTCASE_URI = TEST_BASE_HTTP + "minified.html"; let gUI; diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_sourcemap_watching.js b/browser/devtools/styleeditor/test/browser_styleeditor_sourcemap_watching.js index db16907a6dcd..98f2ffaf57ce 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_sourcemap_watching.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_sourcemap_watching.js @@ -6,11 +6,11 @@ Components.utils.import("resource://gre/modules/Task.jsm"); let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); -const TESTCASE_URI_HTML = TEST_BASE + "sourcemaps-watching.html"; -const TESTCASE_URI_CSS = TEST_BASE + "sourcemap-css/sourcemaps.css"; -const TESTCASE_URI_REG_CSS = TEST_BASE + "simple.css"; -const TESTCASE_URI_SCSS = TEST_BASE + "sourcemap-sass/sourcemaps.scss"; -const TESTCASE_URI_MAP = TEST_BASE + "sourcemap-css/sourcemaps.css.map"; +const TESTCASE_URI_HTML = TEST_BASE_HTTP + "sourcemaps-watching.html"; +const TESTCASE_URI_CSS = TEST_BASE_HTTP + "sourcemap-css/sourcemaps.css"; +const TESTCASE_URI_REG_CSS = TEST_BASE_HTTP + "simple.css"; +const TESTCASE_URI_SCSS = TEST_BASE_HTTP + "sourcemap-sass/sourcemaps.scss"; +const TESTCASE_URI_MAP = TEST_BASE_HTTP + "sourcemap-css/sourcemaps.css.map"; const TESTCASE_SCSS_NAME = "sourcemaps.scss"; const TRANSITIONS_PREF = "devtools.styleeditor.transitions"; diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_sv_keynav.js b/browser/devtools/styleeditor/test/browser_styleeditor_sv_keynav.js index c77c3b5e068d..bfa0ed7b694b 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_sv_keynav.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_sv_keynav.js @@ -9,7 +9,7 @@ // thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); -const TESTCASE_URI = TEST_BASE + "four.html"; +const TESTCASE_URI = TEST_BASE_HTTP + "four.html"; let gUI; diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js b/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js index 0d25da293c1e..32fdc449eb41 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js @@ -2,7 +2,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -const TESTCASE_URI = TEST_BASE + "simple.html"; +const TESTCASE_URI = TEST_BASE_HTTP + "simple.html"; let gOriginalWidth; // these are set by runTests() let gOriginalHeight; diff --git a/browser/devtools/webconsole/test/browser.ini b/browser/devtools/webconsole/test/browser.ini index 4dca6e15ece2..575ce9d03c3b 100644 --- a/browser/devtools/webconsole/test/browser.ini +++ b/browser/devtools/webconsole/test/browser.ini @@ -354,6 +354,7 @@ skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s [browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js] [browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js] +[browser_console_history_persist.js] [browser_webconsole_output_01.js] skip-if = e10s # Bug 1042253 - webconsole e10s tests [browser_webconsole_output_02.js] diff --git a/browser/devtools/webconsole/test/browser_console_history_persist.js b/browser/devtools/webconsole/test/browser_console_history_persist.js new file mode 100644 index 000000000000..1d9b22a05241 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_history_persist.js @@ -0,0 +1,96 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// Test that console command input is persisted across toolbox loads. +// See Bug 943306. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test for persisting history - bug 943306"; +const INPUT_HISTORY_COUNT = 10; + +let test = asyncTest(function* () { + info ("Setting custom input history pref to " + INPUT_HISTORY_COUNT); + Services.prefs.setIntPref("devtools.webconsole.inputHistoryCount", INPUT_HISTORY_COUNT); + + // First tab: run a bunch of commands and then make sure that you can + // navigate through their history. + yield loadTab(TEST_URI); + let hud1 = yield openConsole(); + is (JSON.stringify(hud1.jsterm.history), "[]", "No history on first tab initially"); + yield populateInputHistory(hud1); + is (JSON.stringify(hud1.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]', + "First tab has populated history"); + + // Second tab: Just make sure that you can navigate through the history + // generated by the first tab. + yield loadTab(TEST_URI); + let hud2 = yield openConsole(); + is (JSON.stringify(hud2.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]', + "Second tab has populated history"); + yield testNaviatingHistoryInUI(hud2); + is (JSON.stringify(hud2.jsterm.history), '["0","1","2","3","4","5","6","7","8","9",""]', + "An empty entry has been added in the second tab due to history perusal"); + + // Third tab: Should have the same history as first tab, but if we run a + // command, then the history of the first and second shouldn't be affected + yield loadTab(TEST_URI); + let hud3 = yield openConsole(); + is (JSON.stringify(hud3.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]', + "Third tab has populated history"); + + // Set input value separately from execute so UP arrow accurately navigates history. + hud3.jsterm.setInputValue('"hello from third tab"'); + hud3.jsterm.execute(); + + is (JSON.stringify(hud1.jsterm.history), '["0","1","2","3","4","5","6","7","8","9"]', + "First tab history hasn't changed due to command in third tab"); + is (JSON.stringify(hud2.jsterm.history), '["0","1","2","3","4","5","6","7","8","9",""]', + "Second tab history hasn't changed due to command in third tab"); + is (JSON.stringify(hud3.jsterm.history), '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]', + "Third tab has updated history (and purged the first result) after running a command"); + + // Fourth tab: Should have the latest command from the third tab, followed + // by the rest of the history from the first tab. + yield loadTab(TEST_URI); + let hud4 = yield openConsole(); + is (JSON.stringify(hud4.jsterm.history), '["1","2","3","4","5","6","7","8","9","\\"hello from third tab\\""]', + "Fourth tab has most recent history"); + + info ("Clearing custom input history pref"); + Services.prefs.clearUserPref("devtools.webconsole.inputHistoryCount"); +}); + +/** + * Populate the history by running the following commands: + * [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + */ +function* populateInputHistory(hud) { + let jsterm = hud.jsterm; + let {inputNode} = jsterm; + + for (let i = 0; i < INPUT_HISTORY_COUNT; i++) { + // Set input value separately from execute so UP arrow accurately navigates history. + jsterm.setInputValue(i); + jsterm.execute(); + } +} + +/** + * Check pressing up results in history traversal like: + * [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + */ +function* testNaviatingHistoryInUI(hud) { + let jsterm = hud.jsterm; + let {inputNode} = jsterm; + inputNode.focus(); + + // Count backwards from original input and make sure that pressing up + // restores this. + for (let i = INPUT_HISTORY_COUNT - 1; i >= 0; i--) { + EventUtils.synthesizeKey("VK_UP", {}); + is(inputNode.value, i, "Pressing up restores last input"); + } +} diff --git a/browser/devtools/webconsole/test/head.js b/browser/devtools/webconsole/test/head.js index c49badb40fa7..c8124d0ce4c2 100644 --- a/browser/devtools/webconsole/test/head.js +++ b/browser/devtools/webconsole/test/head.js @@ -13,6 +13,7 @@ let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); let {require, TargetFactory} = devtools; let {Utils: WebConsoleUtils} = require("devtools/toolkit/webconsole/utils"); let {Messages} = require("devtools/webconsole/console-output"); +const asyncStorage = require("devtools/toolkit/shared/async-storage"); // promise._reportErrors = true; // please never leave me. //Services.prefs.setBoolPref("devtools.debugger.log", true); @@ -322,6 +323,9 @@ let finishTest = Task.async(function* () { registerCleanupFunction(function*() { gDevTools.testing = false; + // Remove stored console commands in between tests + yield asyncStorage.removeItem("webConsoleHistory"); + dumpConsoles(); if (HUDService.getBrowserConsole()) { diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index 3c7d022b06e8..54f9ebb07123 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -26,6 +26,8 @@ loader.lazyGetter(this, "ConsoleOutput", () => require("devtools/webconsole/console-output").ConsoleOutput); loader.lazyGetter(this, "Messages", () => require("devtools/webconsole/console-output").Messages); +loader.lazyGetter(this, "asyncStorage", + () => require("devtools/toolkit/shared/async-storage")); loader.lazyImporter(this, "EnvironmentClient", "resource://gre/modules/devtools/dbg-client.jsm"); loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm"); loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm"); @@ -176,6 +178,7 @@ const MIN_FONT_SIZE = 10; const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout"; const PREF_PERSISTLOG = "devtools.webconsole.persistlog"; const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; +const PREF_INPUT_HISTORY_COUNT = "devtools.webconsole.inputHistoryCount"; /** * A WebConsoleFrame instance is an interactive console initialized *per target* @@ -443,12 +446,29 @@ WebConsoleFrame.prototype = { /** * Initialize the WebConsoleFrame instance. * @return object - * A promise object for the initialization. + * A promise object that resolves once the frame is ready to use. */ - init: function WCF_init() + init: function() { this._initUI(); - return this._initConnection(); + 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); + Services.obs.notifyObservers(id, "web-console-created", null); + }; + allReady.then(notifyObservers, notifyObservers); + + return allReady; }, /** @@ -475,9 +495,6 @@ WebConsoleFrame.prototype = { aReason.error + ": " + aReason.message); this.outputMessage(CATEGORY_JS, node, [aReason]); this._initDefer.reject(aReason); - }).then(() => { - let id = WebConsoleUtils.supportsString(this.hudId); - Services.obs.notifyObservers(id, "web-console-created", null); }); return this._initDefer.promise; @@ -3054,17 +3071,11 @@ function JSTerm(aWebConsoleFrame) { this.hud = aWebConsoleFrame; this.hudId = this.hud.hudId; + this.inputHistoryCount = Services.prefs.getIntPref(PREF_INPUT_HISTORY_COUNT); this.lastCompletion = { value: null }; - this.history = []; + this._loadHistory(); - // Holds the number of entries in history. This value is incremented in - // this.execute(). - this.historyIndex = 0; // incremented on this.execute() - - // Holds the index of the history entry that the user is currently viewing. - // This is reset to this.history.length when this.execute() is invoked. - this.historyPlaceHolder = 0; this._objectActorsInVariablesViews = new Map(); this._keyPress = this._keyPress.bind(this); @@ -3079,6 +3090,38 @@ function JSTerm(aWebConsoleFrame) JSTerm.prototype = { SELECTED_FRAME: -1, + /** + * Load the console history from previous sessions. + * @private + */ + _loadHistory: function() { + this.history = []; + this.historyIndex = this.historyPlaceHolder = 0; + + this.historyLoaded = asyncStorage.getItem("webConsoleHistory").then(value => { + if (Array.isArray(value)) { + // Since it was gotten asynchronously, there could be items already in + // the history. It's not likely but stick them onto the end anyway. + this.history = value.concat(this.history); + + // Holds the number of entries in history. This value is incremented in + // this.execute(). + this.historyIndex = this.history.length; + + // Holds the index of the history entry that the user is currently viewing. + // This is reset to this.history.length when this.execute() is invoked. + this.historyPlaceHolder = this.history.length; + } + }, console.error); + }, + + /** + * Stores the console history for future sessions. + */ + storeHistory: function() { + asyncStorage.setItem("webConsoleHistory", this.history); + }, + /** * Stores the data for the last completion. * @type object @@ -3388,6 +3431,12 @@ JSTerm.prototype = { // value that was not evaluated yet. this.history[this.historyIndex++] = aExecuteString; this.historyPlaceHolder = this.history.length; + + if (this.history.length > this.inputHistoryCount) { + this.history.splice(0, this.history.length - this.inputHistoryCount); + this.historyIndex = this.historyPlaceHolder = this.history.length; + } + this.storeHistory(); WebConsoleUtils.usageCount++; this.setInputValue(""); this.clearCompletion(); diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 6b15f029fb21..b3982d04542f 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -476,6 +476,8 @@ @RESPATH@/components/formautofill.manifest @RESPATH@/components/FormAutofillContentService.js @RESPATH@/components/FormAutofillStartup.js +@RESPATH@/components/CSSUnprefixingService.js +@RESPATH@/components/CSSUnprefixingService.manifest @RESPATH@/components/contentAreaDropListener.manifest @RESPATH@/components/contentAreaDropListener.js @RESPATH@/browser/components/BrowserProfileMigrators.manifest diff --git a/browser/themes/shared/devtools/netmonitor.inc.css b/browser/themes/shared/devtools/netmonitor.inc.css index 5e6ba5c53515..6fe7cc1aea42 100644 --- a/browser/themes/shared/devtools/netmonitor.inc.css +++ b/browser/themes/shared/devtools/netmonitor.inc.css @@ -495,9 +495,14 @@ label.requests-menu-status-code { } .tabpanel-summary-value { + color: inherit; -moz-padding-start: 3px; } +.theme-dark .tabpanel-summary-value { + color: var(--theme-selection-color); +} + /* Headers tabpanel */ #headers-summary-status, diff --git a/build/virtualenv_packages.txt b/build/virtualenv_packages.txt index 1dace34d23f2..523c5ba9dad9 100644 --- a/build/virtualenv_packages.txt +++ b/build/virtualenv_packages.txt @@ -26,3 +26,4 @@ gyp.pth:media/webrtc/trunk/tools/gyp/pylib pyasn1.pth:python/pyasn1 bitstring.pth:python/bitstring redo.pth:python/redo +requests.pth:python/requests diff --git a/configure.in b/configure.in index bfb8e43e47ea..7b6c5c44d49c 100644 --- a/configure.in +++ b/configure.in @@ -8892,6 +8892,7 @@ AC_SUBST(LIBJPEG_TURBO_MIPS_ASM) AC_SUBST(MOZ_PACKAGE_JSSHELL) AC_SUBST(MOZ_FOLD_LIBS) +AC_SUBST(SOCORRO_SYMBOL_UPLOAD_TOKEN_FILE) AC_SUBST(MOZ_ENABLE_SZIP) AC_SUBST(MOZ_SZIP_FLAGS) diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index b509d8b57143..d3e7ec0ec5e8 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -13818,10 +13818,6 @@ nsDocShell::GetAppManifestURL(nsAString& aAppManifestURL) NS_IMETHODIMP nsDocShell::GetAsyncPanZoomEnabled(bool* aOut) { - if (TabChild* tabChild = TabChild::GetFrom(this)) { - *aOut = tabChild->IsAsyncPanZoomEnabled(); - return NS_OK; - } *aOut = Preferences::GetBool("layers.async-pan-zoom.enabled", false); return NS_OK; } diff --git a/docshell/test/navigation/mochitest.ini b/docshell/test/navigation/mochitest.ini index 8ba6855cba23..30c3bba6eb82 100644 --- a/docshell/test/navigation/mochitest.ini +++ b/docshell/test/navigation/mochitest.ini @@ -22,9 +22,9 @@ support-files = parent.html [test_bug13871.html] -skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s #RANDOM +skip-if = buildapp == 'b2g' || buildapp == 'mulet' || toolkit == 'android' || e10s #RANDOM # Bug 1136180 disabled on B2G Desktop and Mulet for intermittent failures [test_bug270414.html] -skip-if = buildapp == 'b2g' || toolkit == "android" || e10s +skip-if = buildapp == 'b2g' || buildapp == 'mulet' || toolkit == "android" || e10s # Bug 1136180 disabled on B2G Desktop and Mulet for intermittent failures [test_bug278916.html] skip-if = (buildapp == 'b2g' && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage [test_bug279495.html] diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 433bd91eb0df..81a40fac90f3 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -6772,7 +6772,7 @@ nsContentUtils::GetSelectionInTextControl(Selection* aSelection, { MOZ_ASSERT(aSelection && aRoot); - if (!aSelection->GetRangeCount()) { + if (!aSelection->RangeCount()) { // Nothing selected aOutStartOffset = aOutEndOffset = 0; return; @@ -6833,7 +6833,7 @@ nsContentUtils::GetSelectionBoundingRect(Selection* aSel) res = nsLayoutUtils::TransformFrameRectToAncestor(frame, res, relativeTo); } } else { - int32_t rangeCount = aSel->GetRangeCount(); + int32_t rangeCount = aSel->RangeCount(); nsLayoutUtils::RectAccumulator accumulator; for (int32_t idx = 0; idx < rangeCount; ++idx) { nsRange* range = aSel->GetRangeAt(idx); diff --git a/dom/base/nsDocument.cpp b/dom/base/nsDocument.cpp index da5bda6fe34d..3830d5f0f761 100644 --- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -6199,7 +6199,7 @@ nsDocument::RegisterElement(JSContext* aCx, const nsAString& aType, } if (!aOptions.mPrototype) { - protoObject = JS_NewObjectWithGivenProto(aCx, nullptr, htmlProto, JS::NullPtr()); + protoObject = JS_NewObjectWithGivenProto(aCx, nullptr, htmlProto); if (!protoObject) { rv.Throw(NS_ERROR_UNEXPECTED); return; @@ -6348,15 +6348,12 @@ nsDocument::RegisterElement(JSContext* aCx, const nsAString& aType, // Make sure that the element name matches the name in the definition. // (e.g. a definition for x-button extending button should match //