diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index fd4daab321fd..207418dcd1bc 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -212,6 +212,8 @@ pref("browser.uitour.themeOrigin", "https://addons.mozilla.org/%LOCALE%/firefox/ pref("browser.uitour.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tour/"); // This is used as a regexp match against the page's URL. pref("browser.uitour.readerViewTrigger", "^https:\\/\\/www\\.mozilla\\.org\\/[^\\/]+\\/firefox\\/reading\\/start"); +// How long to show a Hearbeat survey (two hours, in seconds) +pref("browser.uitour.surveyDuration", 7200); pref("browser.customizemode.tip0.shown", false); pref("browser.customizemode.tip0.learnMoreUrl", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/customize"); @@ -527,8 +529,6 @@ pref("privacy.sanitize.sanitizeOnShutdown", false); pref("privacy.sanitize.migrateFx3Prefs", false); -pref("privacy.sanitize.migrateClearSavedPwdsOnExit", false); - pref("privacy.panicButton.enabled", true); pref("network.proxy.share_proxy_settings", false); // use the same proxy settings for all protocols diff --git a/browser/base/content/browser-feeds.js b/browser/base/content/browser-feeds.js index c11a7b6733fc..269f4a83d606 100644 --- a/browser/base/content/browser-feeds.js +++ b/browser/base/content/browser-feeds.js @@ -49,8 +49,7 @@ var FeedHandler = { for (let feedInfo of feeds) { let item = document.createElement(itemNodeType); let baseTitle = feedInfo.title || feedInfo.href; - let labelStr = gNavigatorBundle.getFormattedString("feedShowFeedNew", [baseTitle]); - item.setAttribute("label", labelStr); + item.setAttribute("label", baseTitle); item.setAttribute("feed", feedInfo.href); item.setAttribute("tooltiptext", feedInfo.href); item.setAttribute("crop", "center"); diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index 2eb7437d40e6..d6f796975c9f 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -812,30 +812,6 @@ command="Browser:ShowAllBookmarks" key="manBookmarkKb"/> - - - - - Sanitizer.onShutdown()); - // One time migration to remove support for the clear saved passwords on exit feature. - if (!Services.prefs.getBoolPref("privacy.sanitize.migrateClearSavedPwdsOnExit")) { - let deprecatedPref = "privacy.clearOnShutdown.passwords"; - let doUpdate = Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") && - Services.prefs.prefHasUserValue(deprecatedPref) && - Services.prefs.getBoolPref(deprecatedPref); - if (doUpdate) { - Services.logins.removeAllLogins(); - Services.prefs.setBoolPref("signon.rememberSignons", false); - } - Services.prefs.clearUserPref(deprecatedPref); - Services.prefs.setBoolPref("privacy.sanitize.migrateClearSavedPwdsOnExit", true); - } - // Handle incomplete sanitizations if (Preferences.has(Sanitizer.PREF_SANITIZE_IN_PROGRESS)) { // Firefox crashed during sanitization. diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 4c34e53a9da3..b261cda316a9 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -5503,7 +5503,25 @@ if (event.target.localName == "tab") { this.tabbrowser.removeTab(event.target, {animate: true, byMouse: true}); } else if (event.originalTarget.localName == "box") { - BrowserOpenTab(); + // The user middleclicked an open space on the tabstrip. This could + // be because they intend to open a new tab, but it could also be + // because they just removed a tab and they now middleclicked on the + // resulting space while that tab is closing. In that case, we don't + // want to open a tab. So if we're removing one or more tabs, and + // the tab click is before the end of the last visible tab, we do + // nothing. + if (this.tabbrowser._removingTabs.length) { + let visibleTabs = this.tabbrowser.visibleTabs; + let ltr = (window.getComputedStyle(this, null).direction == "ltr"); + let lastTab = visibleTabs[visibleTabs.length - 1]; + let endOfTab = lastTab.getBoundingClientRect()[ltr ? "right" : "left"]; + if ((ltr && event.clientX > endOfTab) || + (!ltr && event.clientX < endOfTab)) { + BrowserOpenTab(); + } + } else { + BrowserOpenTab(); + } } else { return; } diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml index ca783d563377..be4a971f1404 100644 --- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -37,7 +37,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. + inputmode="url" + xbl:inherits="tooltiptext=inputtooltiptext,value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/> - diff --git a/browser/components/extensions/ext-tabs.js b/browser/components/extensions/ext-tabs.js index da29942bcb92..0cdfa8bed3ec 100644 --- a/browser/components/extensions/ext-tabs.js +++ b/browser/components/extensions/ext-tabs.js @@ -269,13 +269,17 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => { }).api(), create: function(createProperties, callback) { - let url = createProperties.url || aboutNewTabService.newTabURL; - url = context.uri.resolve(url); - function createInWindow(window) { - let tab = window.gBrowser.addTab(url); + let url; + if (createProperties.url !== null) { + url = context.uri.resolve(createProperties.url); + } else { + url = window.BROWSER_NEW_TAB_URL; + } + let tab = window.gBrowser.addTab(url); let active = true; + if (createProperties.active !== null) { active = createProperties.active; } @@ -299,6 +303,7 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => { let window = createProperties.windowId !== null ? WindowManager.getWindow(createProperties.windowId) : WindowManager.topWindow; + if (!window.gBrowser) { let obs = (finishedWindow, topic, data) => { if (finishedWindow != window) { diff --git a/browser/components/migration/EdgeProfileMigrator.js b/browser/components/migration/EdgeProfileMigrator.js index 573852887314..31268a774699 100644 --- a/browser/components/migration/EdgeProfileMigrator.js +++ b/browser/components/migration/EdgeProfileMigrator.js @@ -221,6 +221,12 @@ EdgeProfileMigrator.prototype.__defineGetter__("sourceProfiles", function() { return isWin10OrHigher ? null : []; }); +EdgeProfileMigrator.prototype.__defineGetter__("sourceLocked", function() { + // There is an exclusive lock on some databases. Assume they are locked for now. + return true; +}); + + EdgeProfileMigrator.prototype.classDescription = "Edge Profile Migrator"; EdgeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=edge"; EdgeProfileMigrator.prototype.classID = Components.ID("{62e8834b-2d17-49f5-96ff-56344903a2ae}"); diff --git a/browser/components/uitour/UITour.jsm b/browser/components/uitour/UITour.jsm index e6f58eb99371..7bb6c8ae3a59 100644 --- a/browser/components/uitour/UITour.jsm +++ b/browser/components/uitour/UITour.jsm @@ -15,6 +15,7 @@ Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource:///modules/RecentWindow.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/TelemetryController.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); Cu.importGlobalProperties(["URL"]); @@ -41,6 +42,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "ReaderParent", const PREF_LOG_LEVEL = "browser.uitour.loglevel"; const PREF_SEENPAGEIDS = "browser.uitour.seenPageIDs"; const PREF_READERVIEW_TRIGGER = "browser.uitour.readerViewTrigger"; +const PREF_SURVEY_DURATION = "browser.uitour.surveyDuration"; const BACKGROUND_PAGE_ACTIONS_ALLOWED = new Set([ "forceShowReaderIcon", @@ -1060,7 +1062,8 @@ this.UITour = { * Show the Heartbeat UI to request user feedback. This function reports back to the * caller using |notify|. The notification event name reflects the current status the UI * is in (either "Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed", - * "Heartbeat:LearnMore", "Heartbeat:Engaged" or "Heartbeat:Voted"). + * "Heartbeat:LearnMore", "Heartbeat:Engaged", "Heartbeat:Voted", + * "Heartbeat:SurveyExpired" or "Heartbeat:WindowClosed"). * When a "Heartbeat:Voted" event is notified * the data payload contains a |score| field which holds the rating picked by the user. * Please note that input parameters are already validated by the caller. @@ -1086,16 +1089,115 @@ this.UITour = { * @param {String} [aOptions.learnMoreURL=null] * The learn more URL to open when clicking on the learn more link. No learn more * will be shown if this is an invalid URL. - * @param {String} [aOptions.privateWindowsOnly=false] + * @param {boolean} [aOptions.privateWindowsOnly=false] * Whether the heartbeat UI should only be targeted at a private window (if one exists). * No notifications should be fired when this is true. + * @param {String} [aOptions.surveyId] + * An ID for the survey, reflected in the Telemetry ping. + * @param {Number} [aOptions.surveyVersion] + * Survey's version number, reflected in the Telemetry ping. + * @param {boolean} [aOptions.testing] + * Whether this is a test survey, reflected in the Telemetry ping. */ showHeartbeat(aChromeWindow, aOptions) { - let maybeNotifyHeartbeat = (...aParams) => { + // Initialize survey state + let pingSent = false; + let surveyResults = {}; + let surveyEndTimer = null; + + /** + * Accumulates survey events and submits to Telemetry after the survey ends. + * + * @param {String} aEventName + * Heartbeat event name + * @param {Object} aParams + * Additional parameters and their values + */ + let maybeNotifyHeartbeat = (aEventName, aParams = {}) => { + // Return if event occurred after the ping was sent + if (pingSent) { + log.warn("maybeNotifyHeartbeat: event occurred after ping sent:", aEventName, aParams); + return; + } + + // No Telemetry from private-window-only Heartbeats if (aOptions.privateWindowsOnly) { return; } - this.notify(...aParams); + + let ts = Date.now(); + let sendPing = false; + switch (aEventName) { + case "Heartbeat:NotificationOffered": + surveyResults.flowId = aOptions.flowId; + surveyResults.offeredTS = ts; + break; + case "Heartbeat:LearnMore": + // record only the first click + if (!surveyResults.learnMoreTS) { + surveyResults.learnMoreTS = ts; + } + break; + case "Heartbeat:Engaged": + surveyResults.engagedTS = ts; + break; + case "Heartbeat:Voted": + surveyResults.votedTS = ts; + surveyResults.score = aParams.score; + break; + case "Heartbeat:SurveyExpired": + surveyResults.expiredTS = ts; + break; + case "Heartbeat:NotificationClosed": + // this is the final event in most surveys + surveyResults.closedTS = ts; + sendPing = true; + break; + case "Heartbeat:WindowClosed": + surveyResults.windowClosedTS = ts; + sendPing = true; + break; + default: + log.error("maybeNotifyHeartbeat: unrecognized event:", aEventName); + break; + } + + aParams.timestamp = ts; + aParams.flowId = aOptions.flowId; + this.notify(aEventName, aParams); + + if (!sendPing) { + return; + } + + // Send the ping to Telemetry + let payload = Object.assign({}, surveyResults); + payload.version = 1; + for (let meta of ["surveyId", "surveyVersion", "testing"]) { + if (aOptions.hasOwnProperty(meta)) { + payload[meta] = aOptions[meta]; + } + } + + log.debug("Sending payload to Telemetry: aEventName:", aEventName, + "payload:", payload); + + TelemetryController.submitExternalPing("heartbeat", payload, { + addClientId: true, + addEnvironment: true, + }); + + // only for testing + this.notify("Heartbeat:TelemetrySent", payload); + + // Survey is complete, clear out the expiry timer & survey configuration + if (surveyEndTimer) { + clearTimeout(surveyEndTimer); + surveyEndTimer = null; + } + + pingSent = true; + surveyResults = {}; }; let nb = aChromeWindow.document.getElementById("high-priority-global-notificationbox"); @@ -1106,7 +1208,7 @@ this.UITour = { label: aOptions.engagementButtonLabel, callback: () => { // Let the consumer know user engaged. - maybeNotifyHeartbeat("Heartbeat:Engaged", { flowId: aOptions.flowId, timestamp: Date.now() }); + maybeNotifyHeartbeat("Heartbeat:Engaged"); userEngaged(new Map([ ["type", "button"], @@ -1121,11 +1223,16 @@ this.UITour = { } // Create the notification. Prefix its ID to decrease the chances of collisions. let notice = nb.appendNotification(aOptions.message, "heartbeat-" + aOptions.flowId, - "chrome://browser/skin/heartbeat-icon.svg", nb.PRIORITY_INFO_HIGH, buttons, function() { - // Let the consumer know the notification bar was closed. This also happens - // after voting. - maybeNotifyHeartbeat("Heartbeat:NotificationClosed", { flowId: aOptions.flowId, timestamp: Date.now() }); - }.bind(this)); + "chrome://browser/skin/heartbeat-icon.svg", + nb.PRIORITY_INFO_HIGH, buttons, + (aEventType) => { + if (aEventType != "removed") { + return; + } + // Let the consumer know the notification bar was closed. + // This also happens after voting. + maybeNotifyHeartbeat("Heartbeat:NotificationClosed"); + }); // Get the elements we need to style. let messageImage = @@ -1196,11 +1303,7 @@ this.UITour = { let rating = Number(evt.target.getAttribute("data-score"), 10); // Let the consumer know user voted. - maybeNotifyHeartbeat("Heartbeat:Voted", { - flowId: aOptions.flowId, - score: rating, - timestamp: Date.now(), - }); + maybeNotifyHeartbeat("Heartbeat:Voted", { score: rating }); // Append the score data to the engagement URL. userEngaged(new Map([ @@ -1239,8 +1342,7 @@ this.UITour = { learnMore.className = "text-link"; learnMore.href = learnMoreURL.toString(); learnMore.setAttribute("value", aOptions.learnMoreLabel); - learnMore.addEventListener("click", () => maybeNotifyHeartbeat("Heartbeat:LearnMore", - { flowId: aOptions.flowId, timestamp: Date.now() })); + learnMore.addEventListener("click", () => maybeNotifyHeartbeat("Heartbeat:LearnMore")); frag.appendChild(learnMore); } @@ -1251,10 +1353,23 @@ this.UITour = { messageText.classList.add("heartbeat"); // Let the consumer know the notification was shown. - maybeNotifyHeartbeat("Heartbeat:NotificationOffered", { - flowId: aOptions.flowId, - timestamp: Date.now(), - }); + maybeNotifyHeartbeat("Heartbeat:NotificationOffered"); + + // End the survey if the user quits, closes the window, or + // hasn't responded before expiration. + if (!aOptions.privateWindowsOnly) { + function handleWindowClosed(aTopic) { + maybeNotifyHeartbeat("Heartbeat:WindowClosed"); + aChromeWindow.removeEventListener("SSWindowClosing", handleWindowClosed); + } + aChromeWindow.addEventListener("SSWindowClosing", handleWindowClosed); + + let surveyDuration = Services.prefs.getIntPref(PREF_SURVEY_DURATION) * 1000; + surveyEndTimer = setTimeout(() => { + maybeNotifyHeartbeat("Heartbeat:SurveyExpired"); + nb.removeNotification(notice); + }, surveyDuration); + } }, /** diff --git a/browser/components/uitour/test/browser_UITour_heartbeat.js b/browser/components/uitour/test/browser_UITour_heartbeat.js index e4facb6ae90e..68ee587043cf 100644 --- a/browser/components/uitour/test/browser_UITour_heartbeat.js +++ b/browser/components/uitour/test/browser_UITour_heartbeat.js @@ -10,6 +10,9 @@ var gContentWindow; function test() { UITourTest(); requestLongerTimeout(2); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.uitour.surveyDuration"); + }); } function getHeartbeatNotification(aId, aChromeWindow = window) { @@ -66,6 +69,39 @@ function cleanUpNotification(aId, aChromeWindow = window) { notification.close(); } +/** + * Check telemetry payload for proper format and expected content. + * + * @param aPayload + * The Telemetry payload to verify + * @param aFlowId + * Expected value of the flowId field. + * @param aExpectedFields + * Array of expected fields. No other fields are allowed. + */ +function checkTelemetry(aPayload, aFlowId, aExpectedFields) { + // Basic payload format + is(aPayload.version, 1, "Telemetry ping must have heartbeat version=1"); + is(aPayload.flowId, aFlowId, "Flow ID in the Telemetry ping must match"); + + // Check for superfluous fields + let extraKeys = new Set(Object.keys(aPayload)); + extraKeys.delete("version"); + extraKeys.delete("flowId"); + + // Check for expected fields + for (let field of aExpectedFields) { + ok(field in aPayload, "The payload should have the field '" + field + "'"); + if (field.endsWith("TS")) { + let ts = aPayload[field]; + ok(Number.isInteger(ts) && ts > 0, "Timestamp '" + field + "' must be a natural number"); + } + extraKeys.delete(field); + } + + is(extraKeys.size, 0, "No unexpected fields in the Telemetry payload"); +} + var tests = [ /** * Check that the "stars" heartbeat UI correctly shows and closes. @@ -88,6 +124,11 @@ var tests = [ done(); break; } + case "Heartbeat:TelemetrySent": { + info("'Heartbeat:TelemetrySent' notification received"); + checkTelemetry(aData, flowId, ["offeredTS", "closedTS"]); + break; + } default: // We are not expecting other states for this test. ok(false, "Unexpected notification received: " + aEventName); @@ -125,6 +166,12 @@ var tests = [ done(); break; } + case "Heartbeat:TelemetrySent": { + info("'Heartbeat:TelemetrySent' notification received."); + checkTelemetry(aData, flowId, ["offeredTS", "votedTS", "closedTS", "score"]); + is(aData.score, 2, "Checking Telemetry payload.score"); + break; + } default: // We are not expecting other states for this test. ok(false, "Unexpected notification received: " + aEventName); @@ -163,6 +210,12 @@ var tests = [ done(); break; } + case "Heartbeat:TelemetrySent": { + info("'Heartbeat:TelemetrySent' notification received."); + checkTelemetry(aData, flowId, ["offeredTS", "votedTS", "closedTS", "score"]); + is(aData.score, 2, "Checking Telemetry payload.score"); + break; + } default: // We are not expecting other states for this test. ok(false, "Unexpected notification received: " + aEventName); @@ -200,6 +253,12 @@ var tests = [ done(); break; } + case "Heartbeat:TelemetrySent": { + info("'Heartbeat:TelemetrySent' notification received."); + checkTelemetry(aData, flowId, ["offeredTS", "votedTS", "closedTS", "score"]); + is(aData.score, expectedScore, "Checking Telemetry payload.score"); + break; + } default: // We are not expecting other states for this test. ok(false, "Unexpected notification received: " + aEventName); @@ -243,6 +302,12 @@ var tests = [ done(); break; } + case "Heartbeat:TelemetrySent": { + info("'Heartbeat:TelemetrySent' notification received."); + checkTelemetry(aData, flowId, ["offeredTS", "votedTS", "closedTS", "score"]); + is(aData.score, 1, "Checking Telemetry payload.score"); + break; + } default: // We are not expecting other states for this test. ok(false, "Unexpected notification received: " + aEventName); @@ -290,6 +355,11 @@ var tests = [ executeSoon(done); break; } + case "Heartbeat:TelemetrySent": { + info("'Heartbeat:TelemetrySent' notification received."); + checkTelemetry(aData, flowId, ["offeredTS", "engagedTS", "closedTS"]); + break; + } default: { // We are not expecting other states for this test. ok(false, "Unexpected notification received: " + aEventName); @@ -335,6 +405,11 @@ var tests = [ done(); break; } + case "Heartbeat:TelemetrySent": { + info("'Heartbeat:TelemetrySent' notification received."); + checkTelemetry(aData, flowId, ["offeredTS", "learnMoreTS", "closedTS"]); + break; + } default: // We are not expecting other states for this test. ok(false, "Unexpected notification received: " + aEventName); @@ -456,4 +531,90 @@ var tests = [ yield BrowserTestUtils.closeWindow(privateWin); }), + + /** + * Test that the survey closes itself after a while and submits Telemetry + */ + taskify(function* test_telemetry_surveyExpired() { + let flowId = "survey-expired-" + Math.random(); + let engagementURL = "http://example.com"; + let surveyDuration = 1; // 1 second (pref is in seconds) + Services.prefs.setIntPref("browser.uitour.surveyDuration", surveyDuration); + + let telemetryPromise = new Promise((resolve, reject) => { + gContentAPI.observe(function (aEventName, aData) { + switch (aEventName) { + case "Heartbeat:NotificationOffered": + info("'Heartbeat:NotificationOffered' notification received"); + break; + case "Heartbeat:SurveyExpired": + info("'Heartbeat:SurveyExpired' notification received"); + ok(true, "Survey should end on its own after a time out"); + case "Heartbeat:NotificationClosed": + info("'Heartbeat:NotificationClosed' notification received"); + break; + case "Heartbeat:TelemetrySent": { + info("'Heartbeat:TelemetrySent' notification received"); + checkTelemetry(aData, flowId, ["offeredTS", "expiredTS", "closedTS"]); + resolve(); + break; + } + default: + // not expecting other states for this test + ok(false, "Unexpected notification received: " + aEventName); + reject(); + } + }); + }); + + gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL); + yield telemetryPromise; + Services.prefs.clearUserPref("browser.uitour.surveyDuration"); + }), + + /** + * Check that certain whitelisted experiment parameters get reflected in the + * Telemetry ping + */ + function test_telemetry_params(done) { + let flowId = "telemetry-params-" + Math.random(); + let engagementURL = "http://example.com"; + let extraParams = { + "surveyId": "foo", + "surveyVersion": 1.5, + "testing": true, + "notWhitelisted": 123, + }; + let expectedFields = ["surveyId", "surveyVersion", "testing"]; + + gContentAPI.observe(function (aEventName, aData) { + switch (aEventName) { + case "Heartbeat:NotificationOffered": { + info("'Heartbeat:Offered' notification received (timestamp " + aData.timestamp.toString() + ")."); + cleanUpNotification(flowId); + break; + } + case "Heartbeat:NotificationClosed": { + info("'Heartbeat:NotificationClosed' notification received (timestamp " + aData.timestamp.toString() + ")."); + break; + } + case "Heartbeat:TelemetrySent": { + info("'Heartbeat:TelemetrySent' notification received"); + checkTelemetry(aData, flowId, ["offeredTS", "closedTS"].concat(expectedFields)); + for (let param of expectedFields) { + is(aData[param], extraParams[param], + "Whitelisted experiment configs should be copied into Telemetry pings"); + } + done(); + break; + } + default: + // We are not expecting other states for this test. + ok(false, "Unexpected notification received: " + aEventName); + } + }); + + gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", + flowId, engagementURL, null, null, extraParams); + }, ]; diff --git a/browser/locales/en-US/chrome/browser/browser.dtd b/browser/locales/en-US/chrome/browser/browser.dtd index dca3be740a18..018e90173a64 100644 --- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -163,7 +163,7 @@ These should match what Safari and other Apple applications use on OS X Lion. -- - + diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index 0bbe65090803..9114761fc04e 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -244,9 +244,6 @@ sanitizeSelectedWarning=All selected items will be cleared. update.downloadAndInstallButton.label=Update to %S update.downloadAndInstallButton.accesskey=U -# RSS Pretty Print -feedShowFeedNew=Subscribe to '%S'… - menuOpenAllInTabs.label=Open All in Tabs # History menu diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css index 2fc5de8757de..8a3e9dfed168 100644 --- a/browser/themes/linux/browser.css +++ b/browser/themes/linux/browser.css @@ -508,9 +508,7 @@ menuitem:not([type]):not(.menuitem-tooltip):not(.menuitem-iconic-tooltip) { } #subscribeToPageMenuitem:not([disabled]), -#subscribeToPageMenupopup, -#BMB_subscribeToPageMenuitem:not([disabled]), -#BMB_subscribeToPageMenupopup { +#subscribeToPageMenupopup { list-style-image: url("chrome://browser/skin/page-livemarks.png"); } diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index b7073019cc78..fa0268666158 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -2294,9 +2294,7 @@ notification[value="translation"] { /* Bookmarks roots menu-items */ #subscribeToPageMenuitem:not([disabled]), -#subscribeToPageMenupopup, -#BMB_subscribeToPageMenuitem:not([disabled]), -#BMB_subscribeToPageMenupopup { +#subscribeToPageMenupopup { list-style-image: url("chrome://browser/skin/feeds/feedIcon16.png"); } diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-03.js b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-03.js index 0404e137f96c..995aee165f25 100644 --- a/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-03.js +++ b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-03.js @@ -17,24 +17,43 @@ function test() { const gView = gDebugger.DebuggerView; const gEditor = gDebugger.DebuggerView.editor; const gL10N = gDebugger.L10N; + const require = gDebugger.require; const actions = bindActionCreators(gPanel); + const constants = require("./content/constants"); + const controller = gDebugger.DebuggerController; function showBogusSource() { - let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_ERROR_SHOWN); - actions.newSource({ url: "http://example.com/fake.js", actor: "fake.actor" }); - actions.selectSource({ actor: "fake.actor" }); - return finished; + const source = { actor: "fake.actor", url: "http://fake.url/" }; + actions.newSource(source); + + controller.dispatch({ + type: constants.LOAD_SOURCE_TEXT, + source: source, + status: "start" + }); + + controller.dispatch({ + type: constants.SELECT_SOURCE, + source: source + }); + + controller.dispatch({ + type: constants.LOAD_SOURCE_TEXT, + source: source, + status: "error", + error: "bogus actor" + }); } function testDebuggerLoadingError() { - ok(gEditor.getText().includes(gL10N.getFormatStr("errorLoadingText2", "No such actor for ID: fake.actor")), + ok(gEditor.getText().includes(gL10N.getFormatStr("errorLoadingText2", "")), "The valid error loading message is displayed."); } Task.spawn(function*() { yield waitForSourceShown(gPanel, "-01.js"); - yield showBogusSource(); - yield testDebuggerLoadingError(); + showBogusSource(); + testDebuggerLoadingError(); closeDebuggerAndFinish(gPanel); }); }); diff --git a/devtools/client/framework/test/browser_devtools_api.js b/devtools/client/framework/test/browser_devtools_api.js index 3618bb9e5732..ea263dd551c9 100644 --- a/devtools/client/framework/test/browser_devtools_api.js +++ b/devtools/client/framework/test/browser_devtools_api.js @@ -39,11 +39,11 @@ function runTests1(aTab) { }; ok(gDevTools, "gDevTools exists"); - is(gDevTools.getToolDefinitionMap().has(toolId1), false, + ok(!gDevTools.getToolDefinitionMap().has(toolId1), "The tool is not registered"); gDevTools.registerTool(toolDefinition); - is(gDevTools.getToolDefinitionMap().has(toolId1), true, + ok(gDevTools.getToolDefinitionMap().has(toolId1), "The tool is registered"); let target = TargetFactory.forTab(gBrowser.selectedTab); @@ -98,11 +98,11 @@ function runTests2() { }, }; - is(gDevTools.getToolDefinitionMap().has(toolId2), false, + ok(!gDevTools.getToolDefinitionMap().has(toolId2), "The tool is not registered"); gDevTools.registerTool(toolDefinition); - is(gDevTools.getToolDefinitionMap().has(toolId2), true, + ok(gDevTools.getToolDefinitionMap().has(toolId2), "The tool is registered"); let target = TargetFactory.forTab(gBrowser.selectedTab); @@ -149,7 +149,7 @@ function runTests2() { }); } -function continueTests(toolbox, panel) { +var continueTests = Task.async(function*(toolbox, panel) { ok(toolbox.getCurrentPanel(), "panel value is correct"); is(toolbox.currentToolId, toolId2, "toolbox _currentToolId is correct"); @@ -160,24 +160,42 @@ function continueTests(toolbox, panel) { "The builtin tool tabs do have the invertable attribute"); let toolDefinitions = gDevTools.getToolDefinitionMap(); - is(toolDefinitions.has(toolId2), true, "The tool is in gDevTools"); + ok(toolDefinitions.has(toolId2), "The tool is in gDevTools"); let toolDefinition = toolDefinitions.get(toolId2); is(toolDefinition.id, toolId2, "toolDefinition id is correct"); - gDevTools.unregisterTool(toolId2); - is(gDevTools.getToolDefinitionMap().has(toolId2), false, + info("Testing toolbox tool-unregistered event"); + let toolSelected = toolbox.once("select"); + let unregisteredTool = yield new Promise(resolve => { + toolbox.once("tool-unregistered", (e,id) => resolve(id)); + gDevTools.unregisterTool(toolId2); + }); + yield toolSelected; + + is(unregisteredTool, toolId2, "Event returns correct id"); + ok(!toolbox.isToolRegistered(toolId2), + "Toolbox: The tool is not registered"); + ok(!gDevTools.getToolDefinitionMap().has(toolId2), "The tool is no longer registered"); - // Wait for unregisterTool to select the next tool before - // attempting to destroy. - toolbox.on("select", function selectListener (_, id) { - if (id !== "test-tool") { - toolbox.off("select", selectListener); - destroyToolbox(toolbox); - } + info("Testing toolbox tool-registered event"); + let registeredTool = yield new Promise(resolve => { + toolbox.once("tool-registered", (e,id) => resolve(id)); + gDevTools.registerTool(toolDefinition); }); -} + + is(registeredTool, toolId2, "Event returns correct id"); + ok(toolbox.isToolRegistered(toolId2), + "Toolbox: The tool is registered"); + ok(gDevTools.getToolDefinitionMap().has(toolId2), + "The tool is registered"); + + info("Unregistering tool") + gDevTools.unregisterTool(toolId2); + + destroyToolbox(toolbox); +}); function destroyToolbox(toolbox) { toolbox.destroy().then(function() { diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js index dde5d088c21d..cc50512c6495 100644 --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -1772,6 +1772,16 @@ Toolbox.prototype = { }); }, + /** + * Return if the tool is available as a tab (i.e. if it's checked + * in the options panel). This is different from Toolbox.getPanel - + * a tool could be registered but not yet opened in which case + * isToolRegistered would return true but getPanel would return false. + */ + isToolRegistered: function(toolId) { + return gDevTools.getToolDefinitionMap().has(toolId); + }, + /** * Handler for the tool-registered event. * @param {string} event @@ -1782,6 +1792,9 @@ Toolbox.prototype = { _toolRegistered: function(event, toolId) { let tool = gDevTools.getToolDefinition(toolId); this._buildTabForTool(tool); + // Emit the event so tools can listen to it from the toolbox level + // instead of gDevTools + this.emit("tool-registered", toolId); }, /** @@ -1833,6 +1846,9 @@ Toolbox.prototype = { key.parentNode.removeChild(key); } } + // Emit the event so tools can listen to it from the toolbox level + // instead of gDevTools + this.emit("tool-unregistered", toolId); }, /** diff --git a/devtools/client/performance/components/optimizations-item.js b/devtools/client/performance/components/jit-optimizations-item.js similarity index 86% rename from devtools/client/performance/components/optimizations-item.js rename to devtools/client/performance/components/jit-optimizations-item.js index bdb08e625e82..8ee3c3b73e2b 100644 --- a/devtools/client/performance/components/optimizations-item.js +++ b/devtools/client/performance/components/jit-optimizations-item.js @@ -6,7 +6,11 @@ const { Cu } = require("chrome"); Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm"); const STRINGS_URI = "chrome://devtools/locale/jit-optimizations.properties"; const L10N = new ViewHelpers.L10N(STRINGS_URI); +const { PluralForm } = require("resource://gre/modules/PluralForm.jsm"); const { DOM: dom, PropTypes, createClass, createFactory } = require("devtools/client/shared/vendor/react"); +const { + JITOptimizations, hasSuccessfulOutcome, isSuccessfulOutcome +} = require("devtools/client/performance/modules/logic/jit"); const Frame = createFactory(require("devtools/client/shared/components/frame")); const OPTIMIZATION_FAILURE = L10N.getStr("jit.optimizationFailure"); const JIT_SAMPLES = L10N.getStr("jit.samples"); @@ -17,8 +21,8 @@ const PROPNAME_MAX_LENGTH = 4; const TREE_ROW_HEIGHT = 14; const OPTIMIZATION_ITEM_TYPES = ["site", "attempts", "types", "attempt", "type", "observedtype"]; -const OptimizationsItem = module.exports = createClass({ - displayName: "OptimizationsItem", +const JITOptimizationsItem = module.exports = createClass({ + displayName: "JITOptimizationsItem", propTypes: { onViewSourceInDebugger: PropTypes.func.isRequired, @@ -73,18 +77,21 @@ const OptimizationsItem = module.exports = createClass({ } let sampleString = PluralForm.get(site.samples, JIT_SAMPLES).replace("#1", site.samples); - let text = `${lastStrategy}${propString} – (${sampleString})`; + let text = dom.span( + { className: "optimization-site-title" }, + `${lastStrategy}${propString} – (${sampleString})` + ); let frame = Frame({ onClick: () => onViewSourceInDebugger(frameData.url, site.data.line), frame: { source: frameData.url, - line: site.data.line, + line: +site.data.line, column: site.data.column, } }) let children = [text, frame]; - if (!site.hasSuccessfulOutcome()) { + if (!hasSuccessfulOutcome(site)) { children.unshift(dom.span({ className: "opt-icon warning" })); } @@ -104,7 +111,7 @@ const OptimizationsItem = module.exports = createClass({ }, _renderAttempt({ item: attempt }) { - let success = JITOptimizations.isSuccessfulOutcome(attempt.outcome); + let success = isSuccessfulOutcome(attempt.outcome); let { strategy, outcome } = attempt; return dom.span({ className: "optimization-attempt" }, dom.span({ className: "optimization-strategy" }, strategy), @@ -119,7 +126,8 @@ const OptimizationsItem = module.exports = createClass({ _renderObservedType({ onViewSourceInDebugger, item: type }) { let children = [ - `${type.keyedBy}${type.name ? ` → ${type.name}` : ""}` + dom.span({ className: "optimization-observed-type-keyed" }, + `${type.keyedBy}${type.name ? ` → ${type.name}` : ""}`) ]; // If we have a line and location, make a link to the debugger diff --git a/devtools/client/performance/components/optimizations.js b/devtools/client/performance/components/jit-optimizations.js similarity index 81% rename from devtools/client/performance/components/optimizations.js rename to devtools/client/performance/components/jit-optimizations.js index 2490110bb89b..0be0aadcef25 100644 --- a/devtools/client/performance/components/optimizations.js +++ b/devtools/client/performance/components/jit-optimizations.js @@ -9,7 +9,7 @@ const L10N = new ViewHelpers.L10N(STRINGS_URI); const { assert } = require("devtools/shared/DevToolsUtils"); const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react"); const Tree = createFactory(require("../../shared/components/tree")); -const OptimizationsItem = createFactory(require("./optimizations-item")); +const OptimizationsItem = createFactory(require("./jit-optimizations-item")); const FrameView = createFactory(require("../../shared/components/frame")); const onClickTooltipString = frame => @@ -19,13 +19,45 @@ const JIT_TITLE = L10N.getStr("jit.title"); // in `devtools/client/themes/jit-optimizations.css` const TREE_ROW_HEIGHT = 14; -const Optimizations = module.exports = createClass({ - displayName: "Optimizations", +const optimizationAttemptModel = { + id: PropTypes.number.isRequired, + strategy: PropTypes.string.isRequired, + outcome: PropTypes.string.isRequired, +}; + +const optimizationObservedTypeModel = { + keyedBy: PropTypes.string.isRequired, + name: PropTypes.string, + location: PropTypes.string, + line: PropTypes.string, +}; + +const optimizationIonTypeModel = { + id: PropTypes.number.isRequired, + typeset: PropTypes.arrayOf(optimizationObservedTypeModel), + site: PropTypes.number.isRequired, + mirType: PropTypes.number.isRequired, +}; + +const optimizationSiteModel = { + id: PropTypes.number.isRequired, + propertyName: PropTypes.string, + line: PropTypes.number.isRequired, + column: PropTypes.number.isRequired, + data: PropTypes.shape({ + attempts: PropTypes.arrayOf(optimizationAttemptModel).isRequired, + types: PropTypes.arrayOf(optimizationIonTypeModel).isRequired, + }).isRequired, +}; + +const JITOptimizations = module.exports = createClass({ + displayName: "JITOptimizations", propTypes: { onViewSourceInDebugger: PropTypes.func.isRequired, frameData: PropTypes.object.isRequired, - optimizationSites: PropTypes.array.isRequired, + optimizationSites: PropTypes.arrayOf(optimizationSiteModel).isRequired, + autoExpandDepth: PropTypes.number, }, getInitialState() { @@ -35,7 +67,9 @@ const Optimizations = module.exports = createClass({ }, getDefaultProps() { - return {}; + return { + autoExpandDepth: 0 + }; }, render() { @@ -82,7 +116,7 @@ const Optimizations = module.exports = createClass({ }, _createTree(props) { - let { frameData, onViewSourceInDebugger, optimizationSites: sites } = this.props; + let { autoExpandDepth, frameData, onViewSourceInDebugger, optimizationSites: sites } = this.props; let getSite = id => sites.find(site => site.id === id); let getIonTypeForObserved = type => @@ -124,7 +158,7 @@ const Optimizations = module.exports = createClass({ }; return Tree({ - autoExpandDepth: 0, + autoExpandDepth, getParent: node => { let site = getSite(node.id); let parent; diff --git a/devtools/client/performance/components/moz.build b/devtools/client/performance/components/moz.build index 8c62201a9c92..38bc60732634 100644 --- a/devtools/client/performance/components/moz.build +++ b/devtools/client/performance/components/moz.build @@ -4,6 +4,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. DevToolsModules( - 'optimizations-item.js', - 'optimizations.js', + 'jit-optimizations-item.js', + 'jit-optimizations.js', ) + +MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini'] diff --git a/devtools/client/performance/components/test/chrome.ini b/devtools/client/performance/components/test/chrome.ini new file mode 100644 index 000000000000..5ba24a9af782 --- /dev/null +++ b/devtools/client/performance/components/test/chrome.ini @@ -0,0 +1,5 @@ +[DEFAULT] +support-files = + head.js + +[test_jit_optimizations_01.html] diff --git a/devtools/client/performance/components/test/head.js b/devtools/client/performance/components/test/head.js new file mode 100644 index 000000000000..545298c9df71 --- /dev/null +++ b/devtools/client/performance/components/test/head.js @@ -0,0 +1,181 @@ +/* Any copyright is dedicated to the Public Domain. + yield new Promise(function(){}); + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://testing-common/Assert.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +var { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +var { require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {}); +var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {}); +var DevToolsUtils = require("devtools/shared/DevToolsUtils"); +var { TargetFactory } = require("devtools/client/framework/target"); +var { Toolbox } = require("devtools/client/framework/toolbox"); + +DevToolsUtils.testing = true; +var { require: browserRequire } = BrowserLoader("resource://devtools/client/performance/", this); + +var $ = (selector, scope=document) => scope.querySelector(selector); +var $$ = (selector, scope=document) => scope.querySelectorAll(selector); + +function forceRender(comp) { + return setState(comp, {}) + .then(() => setState(comp, {})); +} + +// All tests are asynchronous. +SimpleTest.waitForExplicitFinish(); + +function onNextAnimationFrame(fn) { + return () => + requestAnimationFrame(() => + requestAnimationFrame(fn)); +} + +function setState(component, newState) { + var deferred = promise.defer(); + component.setState(newState, onNextAnimationFrame(deferred.resolve)); + return deferred.promise; +} + +function setProps(component, newState) { + var deferred = promise.defer(); + component.setProps(newState, onNextAnimationFrame(deferred.resolve)); + return deferred.promise; +} + +function dumpn(msg) { + dump(`PERFORMANCE-COMPONENT-TEST: ${msg}\n`); +} + +/** + * Default opts data for testing. First site has a simple IonType, + * and an IonType with an ObservedType, and a successful outcome. + * Second site does not have a successful outcome. + */ +let OPTS_DATA_GENERAL = [{ + id: 1, + propertyName: "my property name", + line: 100, + column: 200, + samples: 90, + data: { + attempts: [ + { id: 1, strategy: "GetElem_TypedObject", outcome: "AccessNotTypedObject" }, + { id: 1, strategy: "GetElem_Dense", outcome: "AccessNotDense" }, + { id: 1, strategy: "GetElem_TypedStatic", outcome: "Disabled" }, + { id: 1, strategy: "GetElem_TypedArray", outcome: "GenericSuccess" }, + ], + types: [{ + id: 1, + site: "Receiver", + mirType: "Object", + typeset: [{ + id: 1, + keyedBy: "constructor", + name: "MyView", + location: "http://internet.com/file.js", + line: "123", + }] + }, { + id: 1, + typeset: void 0, + site: "Index", + mirType: "Int32", + }] + } +}, { + id: 2, + propertyName: void 0, + line: 50, + column: 51, + samples: 100, + data: { + attempts: [ + { id: 2, strategy: "Call_Inline", outcome: "CantInlineBigData" } + ], + types: [{ + id: 2, + site: "Call_Target", + mirType: "Object", + typeset: [ + { id: 2, keyedBy: "primitive" }, + { id: 2, keyedBy: "constructor", name: "B", location: "http://mypage.com/file.js", line: "2" }, + { id: 2, keyedBy: "constructor", name: "C", location: "http://mypage.com/file.js", line: "3" }, + { id: 2, keyedBy: "constructor", name: "D", location: "http://mypage.com/file.js", line: "4" }, + ], + }] + } +}]; +OPTS_DATA_GENERAL.forEach(site => { + site.data.types.forEach(type => { + if (type.typeset) { + type.typeset.id = site.id; + } + }); + site.data.attempts.id = site.id; + site.data.types.id = site.id; +}); + + +function checkOptimizationHeader (name, file, line) { + is($(".optimization-header .header-function-name").textContent, name, + "correct optimization header function name"); + is($(".optimization-header .frame-link-filename").textContent, file, + "correct optimization header file name"); + is($(".optimization-header .frame-link-line").textContent, line, + "correct optimization header line"); +} + +function checkOptimizationTree (rowData) { + let rows = $$(".tree .tree-node"); + + for (let i = 0; i < rowData.length; i++) { + let row = rows[i]; + let expected = rowData[i]; + + switch (expected.type) { + case "site": + is($(".optimization-site-title", row).textContent, + `${expected.strategy} – (${expected.samples} samples)`, + `row ${i}th: correct optimization site row`); + + is(!!$(".opt-icon.warning", row), !!expected.failureIcon, + `row ${i}th: expected visibility of failure icon for unsuccessful outcomes`); + break; + case "types": + is($(".optimization-types", row).textContent, + `Types (${expected.count})`, + `row ${i}th: correct types row`); + break; + case "attempts": + is($(".optimization-attempts", row).textContent, + `Attempts (${expected.count})`, + `row ${i}th: correct attempts row`); + break; + case "type": + is($(".optimization-ion-type", row).textContent, + `${expected.site}:${expected.mirType}`, + `row ${i}th: correct ion type row`); + break; + case "observedtype": + is($(".optimization-observed-type-keyed", row).textContent, + expected.name ? + `${expected.keyedBy} → ${expected.name}` : + expected.keyedBy, + `row ${i}th: correct observed type row`); + break; + case "attempt": + is($(".optimization-strategy", row).textContent, expected.strategy, + `row ${i}th: correct attempt row, attempt item`); + is($(".optimization-outcome", row).textContent, expected.outcome, + `row ${i}th: correct attempt row, outcome item`); + ok($(".optimization-outcome", row).classList.contains(expected.success ? "success" : "failure"), + `row ${i}th: correct attempt row, failure/success status`); + break; + } + } +} + diff --git a/devtools/client/performance/components/test/test_jit_optimizations_01.html b/devtools/client/performance/components/test/test_jit_optimizations_01.html new file mode 100644 index 000000000000..edc9c34cd81a --- /dev/null +++ b/devtools/client/performance/components/test/test_jit_optimizations_01.html @@ -0,0 +1,70 @@ + + + + + + JITOptimizations component test + + + + +
+
+
+
+ + diff --git a/devtools/client/performance/modules/logic/jit.js b/devtools/client/performance/modules/logic/jit.js index a567d4314bd6..d59e998eb99c 100644 --- a/devtools/client/performance/modules/logic/jit.js +++ b/devtools/client/performance/modules/logic/jit.js @@ -120,41 +120,6 @@ const OptimizationSite = function (id, opts) { this.samples = 1; }; -/** - * Returns a boolean indicating if the passed in OptimizationSite - * has a "good" outcome at the end of its attempted strategies. - * - * @param {Array} stringTable - * @return {boolean} - */ - -OptimizationSite.prototype.hasSuccessfulOutcome = function () { - let attempts = this.getAttempts(); - let lastOutcome = attempts[attempts.length - 1].outcome; - return OptimizationSite.isSuccessfulOutcome(lastOutcome); -}; - -/** - * Returns the last attempted OptimizationAttempt for this OptimizationSite. - * - * @return {Array} - */ - -OptimizationSite.prototype.getAttempts = function () { - return this.data.attempts; -}; - -/** - * Returns all IonTypes in this OptimizationSite. - * - * @return {Array} - */ - -OptimizationSite.prototype.getIonTypes = function () { - return this.data.types; -}; - - /** * Constructor for JITOptimizations. A collection of OptimizationSites for a frame. * @@ -240,10 +205,24 @@ JITOptimizations.prototype = { * @return {boolean} */ -OptimizationSite.isSuccessfulOutcome = JITOptimizations.isSuccessfulOutcome = function (outcome) { +function isSuccessfulOutcome (outcome) { return !!~SUCCESSFUL_OUTCOMES.indexOf(outcome); }; +/** + * Takes an OptimizationSite. Returns a boolean indicating if the passed + * in OptimizationSite has a "good" outcome at the end of its attempted strategies. + * + * @param {OptimizationSite} optimizationSite + * @return {boolean} + */ + +function hasSuccessfulOutcome (optimizationSite) { + let attempts = optimizationSite.data.attempts; + let lastOutcome = attempts[attempts.length - 1].outcome; + return isSuccessfulOutcome(lastOutcome); +}; + function maybeString(stringTable, index) { return index ? stringTable[index] : undefined; } @@ -355,3 +334,6 @@ function createTierGraphDataFromFrameNode (frameNode, sampleTimes, bucketSize) { exports.createTierGraphDataFromFrameNode = createTierGraphDataFromFrameNode; exports.OptimizationSite = OptimizationSite; exports.JITOptimizations = JITOptimizations; +exports.hasSuccessfulOutcome = hasSuccessfulOutcome; +exports.isSuccessfulOutcome = isSuccessfulOutcome; +exports.SUCCESSFUL_OUTCOMES = SUCCESSFUL_OUTCOMES; diff --git a/devtools/client/performance/performance-controller.js b/devtools/client/performance/performance-controller.js index 577d559aa768..6b5738fad02b 100644 --- a/devtools/client/performance/performance-controller.js +++ b/devtools/client/performance/performance-controller.js @@ -20,7 +20,7 @@ Object.defineProperty(this, "EVENTS", { var React = require("devtools/client/shared/vendor/react"); var ReactDOM = require("devtools/client/shared/vendor/react-dom"); -var Optimizations = React.createFactory(require("devtools/client/performance/components/optimizations")); +var JITOptimizationsView = React.createFactory(require("devtools/client/performance/components/jit-optimizations")); var Services = require("Services"); var promise = require("promise"); var EventEmitter = require("devtools/shared/event-emitter"); @@ -43,7 +43,6 @@ var FrameUtils = require("devtools/client/performance/modules/logic/frame-utils" var { CallView } = require("devtools/client/performance/modules/widgets/tree-view"); var { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model"); var { FrameNode } = require("devtools/client/performance/modules/logic/tree-model"); -var { JITOptimizations } = require("devtools/client/performance/modules/logic/jit"); // Widgets modules @@ -53,7 +52,6 @@ var { TreeWidget } = require("devtools/client/shared/widgets/TreeWidget"); var { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm"); var { setNamedTimeout, clearNamedTimeout } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm"); -var { PluralForm } = require("resource://gre/modules/PluralForm.jsm"); var BRANCH_NAME = "devtools.performance.ui."; diff --git a/devtools/client/performance/test/unit/test_jit-model-02.js b/devtools/client/performance/test/unit/test_jit-model-02.js index e66b0d29dd12..2dbde89862d7 100644 --- a/devtools/client/performance/test/unit/test_jit-model-02.js +++ b/devtools/client/performance/test/unit/test_jit-model-02.js @@ -3,7 +3,7 @@ /** * Tests that JITOptimizations create OptimizationSites, and the underlying - * OptimizationSites methods work as expected. + * hasSuccessfulOutcome/isSuccessfulOutcome work as intended. */ function run_test() { @@ -11,7 +11,9 @@ function run_test() { } add_task(function test() { - let { JITOptimizations, OptimizationSite } = require("devtools/client/performance/modules/logic/jit"); + let { + JITOptimizations, hasSuccessfulOutcome, isSuccessfulOutcome, SUCCESSFUL_OUTCOMES + } = require("devtools/client/performance/modules/logic/jit"); let rawSites = []; rawSites.push(gRawSite2); @@ -27,22 +29,27 @@ add_task(function test() { let [first, second, third] = sites; /* hasSuccessfulOutcome */ - equal(first.hasSuccessfulOutcome(), false, "optSite.hasSuccessfulOutcome() returns expected (1)"); - equal(second.hasSuccessfulOutcome(), true, "optSite.hasSuccessfulOutcome() returns expected (2)"); - equal(third.hasSuccessfulOutcome(), true, "optSite.hasSuccessfulOutcome() returns expected (3)"); + equal(hasSuccessfulOutcome(first), false, "hasSuccessfulOutcome() returns expected (1)"); + equal(hasSuccessfulOutcome(second), true, "hasSuccessfulOutcome() returns expected (2)"); + equal(hasSuccessfulOutcome(third), true, "hasSuccessfulOutcome() returns expected (3)"); - /* getAttempts */ - equal(first.getAttempts().length, 2, "optSite.getAttempts() has the correct amount of attempts (1)"); - equal(second.getAttempts().length, 5, "optSite.getAttempts() has the correct amount of attempts (2)"); - equal(third.getAttempts().length, 3, "optSite.getAttempts() has the correct amount of attempts (3)"); + /* .data.attempts */ + equal(first.data.attempts.length, 2, "optSite.data.attempts has the correct amount of attempts (1)"); + equal(second.data.attempts.length, 5, "optSite.data.attempts has the correct amount of attempts (2)"); + equal(third.data.attempts.length, 3, "optSite.data.attempts has the correct amount of attempts (3)"); - /* getIonTypes */ - equal(first.getIonTypes().length, 1, "optSite.getIonTypes() has the correct amount of IonTypes (1)"); - equal(second.getIonTypes().length, 2, "optSite.getIonTypes() has the correct amount of IonTypes (2)"); - equal(third.getIonTypes().length, 1, "optSite.getIonTypes() has the correct amount of IonTypes (3)"); + /* .data.types */ + equal(first.data.types.length, 1, "optSite.data.types has the correct amount of IonTypes (1)"); + equal(second.data.types.length, 2, "optSite.data.types has the correct amount of IonTypes (2)"); + equal(third.data.types.length, 1, "optSite.data.types has the correct amount of IonTypes (3)"); + + /* isSuccessfulOutcome */ + ok(SUCCESSFUL_OUTCOMES.length, "Have some successful outcomes in SUCCESSFUL_OUTCOMES"); + SUCCESSFUL_OUTCOMES.forEach(outcome => + ok(isSuccessfulOutcome(outcome), + `${outcome} considered a successful outcome via isSuccessfulOutcome()`)); }); - var gStringTable = new RecordingUtils.UniqueStrings(); function uniqStr(s) { diff --git a/devtools/client/performance/views/details-js-call-tree.js b/devtools/client/performance/views/details-js-call-tree.js index 68d05524014f..a0355e6eb058 100644 --- a/devtools/client/performance/views/details-js-call-tree.js +++ b/devtools/client/performance/views/details-js-call-tree.js @@ -94,7 +94,7 @@ var JsCallTreeView = Heritage.extend(DetailsSubview, { ? frameNode.getOptimizations().optimizationSites : []; - let optimizations = Optimizations({ + let optimizations = JITOptimizationsView({ frameData, optimizationSites, onViewSourceInDebugger: (url, line) => { diff --git a/devtools/client/preferences/devtools.js b/devtools/client/preferences/devtools.js index efb6dbd670d8..445fd6860c44 100644 --- a/devtools/client/preferences/devtools.js +++ b/devtools/client/preferences/devtools.js @@ -329,11 +329,12 @@ pref("devtools.fontinspector.enabled", true); // version for each user. pref("devtools.telemetry.tools.opened.version", "{}"); -// Enable the JSON View tool (an inspector for application/json documents) -#ifdef MOZ_DEV_EDITION - pref("devtools.jsonview.enabled", true); +// Enable the JSON View tool (an inspector for application/json documents) on +// Nightly and Dev. Edition. +#ifdef RELEASE_BUILD +pref("devtools.jsonview.enabled", false); #else - pref("devtools.jsonview.enabled", false); +pref("devtools.jsonview.enabled", true); #endif // Disable the HTML responsive design tool by default. Currently disabled until diff --git a/devtools/client/shared/components/tree.js b/devtools/client/shared/components/tree.js index 8d714a61e8f5..bf441ad3d518 100644 --- a/devtools/client/shared/components/tree.js +++ b/devtools/client/shared/components/tree.js @@ -198,6 +198,7 @@ const Tree = module.exports = createClass({ componentWillReceiveProps(nextProps) { this._autoExpand(); + this._updateHeight(); }, _autoExpand() { @@ -218,7 +219,7 @@ const Tree = module.exports = createClass({ this.state.seen.add(item); for (let child of this.props.getChildren(item)) { - autoExpand(item, currentDepth + 1); + autoExpand(child, currentDepth + 1); } }; diff --git a/devtools/client/webconsole/webconsole.js b/devtools/client/webconsole/webconsole.js index c025fb630084..3626684ce3eb 100644 --- a/devtools/client/webconsole/webconsole.js +++ b/devtools/client/webconsole/webconsole.js @@ -8,7 +8,8 @@ const {Cc, Ci, Cu} = require("chrome"); -const {Utils: WebConsoleUtils, CONSOLE_WORKER_IDS} = require("devtools/shared/webconsole/utils"); +const {Utils: WebConsoleUtils, CONSOLE_WORKER_IDS} = + require("devtools/shared/webconsole/utils"); const promise = require("promise"); const Debugger = require("Debugger"); @@ -69,8 +70,10 @@ const CATEGORY_NETWORK = 0; const CATEGORY_CSS = 1; const CATEGORY_JS = 2; const CATEGORY_WEBDEV = 3; -const CATEGORY_INPUT = 4; // always on -const CATEGORY_OUTPUT = 5; // always on +// always on +const CATEGORY_INPUT = 4; +// always on +const CATEGORY_OUTPUT = 5; const CATEGORY_SECURITY = 6; const CATEGORY_SERVER = 7; @@ -102,20 +105,30 @@ const SEVERITY_CLASS_FRAGMENTS = [ ]; // The preference keys to use for each category/severity combination, indexed -// first by category (rows) and then by severity (columns). +// first by category (rows) and then by severity (columns) in the following +// order: +// +// [ Error, Warning, Info, Log ] // // Most of these rather idiosyncratic names are historical and predate the // division of message type into "category" and "severity". const MESSAGE_PREFERENCE_KEYS = [ -// Error Warning Info Log - [ "network", "netwarn", "netxhr", "networkinfo", ], // Network - [ "csserror", "cssparser", null, "csslog", ], // CSS - [ "exception", "jswarn", null, "jslog", ], // JS - [ "error", "warn", "info", "log", ], // Web Developer - [ null, null, null, null, ], // Input - [ null, null, null, null, ], // Output - [ "secerror", "secwarn", null, null, ], // Security - [ "servererror", "serverwarn", "serverinfo", "serverlog", ], // Server Logging + // Network + [ "network", "netwarn", "netxhr", "networkinfo", ], + // CSS + [ "csserror", "cssparser", null, "csslog", ], + // JS + [ "exception", "jswarn", null, "jslog", ], + // Web Developer + [ "error", "warn", "info", "log", ], + // Input + [ null, null, null, null, ], + // Output + [ null, null, null, null, ], + // Security + [ "secerror", "secwarn", null, null, ], + // Server Logging + [ "servererror", "serverwarn", "serverinfo", "serverlog", ], ]; // A mapping from the console API log event levels to the Web Console @@ -142,7 +155,8 @@ const LEVELS = { // This array contains the prefKey for the workers and it must keep them in the // same order as CONSOLE_WORKER_IDS -const WORKERTYPES_PREFKEYS = [ 'sharedworkers', 'serviceworkers', 'windowlessworkers' ]; +const WORKERTYPES_PREFKEYS = + [ "sharedworkers", "serviceworkers", "windowlessworkers" ]; // The lowest HTTP response code (inclusive) that is considered an error. const MIN_HTTP_ERROR_CODE = 400; @@ -160,20 +174,21 @@ const GROUP_INDENT = 12; // too many messages at once we slow down the Firefox UI too much. const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT; -// The delay between display updates - tells how often we should *try* to push -// new messages to screen. This value is optimistic, updates won't always -// happen. Keep this low so the Web Console output feels live. -const OUTPUT_INTERVAL = 20; // milliseconds +// The delay (in milliseconds) between display updates - tells how often we +// should *try* to push new messages to screen. This value is optimistic, +// updates won't always happen. Keep this low so the Web Console output feels +// live. +const OUTPUT_INTERVAL = 20; -// The maximum amount of time that can be spent doing cleanup inside of the -// flush output callback. If things don't get cleaned up in this time, -// then it will start again the next time it is called. -const MAX_CLEANUP_TIME = 10; // milliseconds +// The maximum amount of time (in milliseconds) that can be spent doing cleanup +// inside of the flush output callback. If things don't get cleaned up in this +// time, then it will start again the next time it is called. +const MAX_CLEANUP_TIME = 10; // When the output queue has more than MESSAGES_IN_INTERVAL items we throttle // output updates to this number of milliseconds. So during a lot of output we // update every N milliseconds given here. -const THROTTLE_UPDATES = 1000; // milliseconds +const THROTTLE_UPDATES = 1000; // The preference prefix for all of the Web Console filters. const FILTER_PREFS_PREFIX = "devtools.webconsole.filter."; @@ -416,7 +431,8 @@ WebConsoleFrame.prototype = { // when the original top level window we attached to is closed, // but we don't want to reset console history and just switch to // the next available window. - return this.owner._browserConsole || Services.prefs.getBoolPref(PREF_PERSISTLOG); + return this.owner._browserConsole || + Services.prefs.getBoolPref(PREF_PERSISTLOG); }, /** @@ -462,9 +478,11 @@ WebConsoleFrame.prototype = { this._initDefer = promise.defer(); this.proxy = new WebConsoleConnectionProxy(this, this.owner.target); - this.proxy.connect().then(() => { // on success + this.proxy.connect().then(() => { + // on success this._initDefer.resolve(this); - }, (reason) => { // on failure + }, (reason) => { + // on failure let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR, reason.error + ": " + reason.message); this.outputMessage(CATEGORY_JS, node, [reason]); @@ -509,7 +527,8 @@ WebConsoleFrame.prototype = { this._initFilterButtons(); let fontSize = this.owner._browserConsole ? - Services.prefs.getIntPref("devtools.webconsole.fontSize") : 0; + Services.prefs.getIntPref("devtools.webconsole.fontSize") : + 0; if (fontSize != 0) { fontSize = Math.max(MIN_FONT_SIZE, fontSize); @@ -530,7 +549,8 @@ WebConsoleFrame.prototype = { // calculations. this._updateCharSize(); - let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0]; + let clearButton = + doc.getElementsByClassName("webconsole-clear-console-button")[0]; clearButton.addEventListener("command", () => { this.owner._onClearButton(); this.jsterm.clearOutput(true); @@ -724,10 +744,12 @@ WebConsoleFrame.prototype = { let accesskey = net.getAttribute("accesskeyMacOSX"); net.setAttribute("accesskey", accesskey); - let logging = this.document.querySelector("toolbarbutton[category=logging]"); + let logging = + this.document.querySelector("toolbarbutton[category=logging]"); logging.removeAttribute("accesskey"); - let serverLogging = this.document.querySelector("toolbarbutton[category=server]"); + let serverLogging = + this.document.querySelector("toolbarbutton[category=server]"); serverLogging.removeAttribute("accesskey"); } }, @@ -814,7 +836,7 @@ WebConsoleFrame.prototype = { let target = event.target; let tagName = target.tagName; // Prevent toggle if generated from a contextmenu event (right click) - let isRightClick = (event.button === 2); // right click is button 2; + let isRightClick = (event.button === 2); if (tagName != event.currentTarget.tagName || isRightClick) { return; } @@ -863,8 +885,8 @@ WebConsoleFrame.prototype = { this._setMenuState(target, state); // CSS reflow logging can decrease web page performance. - // Make sure the option is always unchecked when the CSS filter button is selected. - // See bug 971798. + // Make sure the option is always unchecked when the CSS filter button + // is selected. See bug 971798. if (target.getAttribute("category") == "css" && state) { let csslogMenuItem = target.querySelector("menuitem[prefKey=csslog]"); csslogMenuItem.setAttribute("checked", false); @@ -976,7 +998,7 @@ WebConsoleFrame.prototype = { let searchStr = str.toLowerCase(); let filterStrings = filter.toLowerCase().split(/\s+/); - return !filterStrings.some(function (f) { + return !filterStrings.some(function(f) { return searchStr.indexOf(f) == -1; }); }, @@ -1096,7 +1118,8 @@ WebConsoleFrame.prototype = { mergeFilteredMessageNode: function(original, filtered) { let repeatNode = original.getElementsByClassName("message-repeats")[0]; if (!repeatNode) { - return; // no repeat node, return early. + // no repeat node, return early. + return; } let occurrences = parseInt(repeatNode.getAttribute("value")) + 1; @@ -1141,7 +1164,8 @@ WebConsoleFrame.prototype = { return null; } - let lastRepeatNode = lastMessage.getElementsByClassName("message-repeats")[0]; + let lastRepeatNode = + lastMessage.getElementsByClassName("message-repeats")[0]; if (lastRepeatNode && lastRepeatNode._uid == uid) { dupeNode = lastMessage; } @@ -1329,7 +1353,8 @@ WebConsoleFrame.prototype = { } if (level == "groupEnd") { - return null; // no need to continue + // no need to continue + return null; } if (!node) { @@ -1388,7 +1413,7 @@ WebConsoleFrame.prototype = { severity = 'log'; } - switch(category) { + switch (category) { case CATEGORY_CSS: category = 'css'; break; @@ -1421,7 +1446,8 @@ WebConsoleFrame.prototype = { // it makes sense to only display the protcol, host and port (prePath). // This also means messages are grouped for a single origin. if (scriptError.category && scriptError.category == "SHA-1 Signature") { - let sourceURI = Services.io.newURI(scriptError.sourceName, null, null).QueryInterface(Ci.nsIURL); + let sourceURI = Services.io.newURI(scriptError.sourceName, null, null) + .QueryInterface(Ci.nsIURL); displayOrigin = sourceURI.prePath; } @@ -1592,7 +1618,8 @@ WebConsoleFrame.prototype = { * Parent to the requested urlNode. */ makeMixedContentNode: function(linkNode) { - let mixedContentWarning = "[" + l10n.getStr("webConsoleMixedContentWarning") + "]"; + let mixedContentWarning = + "[" + l10n.getStr("webConsoleMixedContentWarning") + "]"; // Mixed content warning message links to a Learn More page let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a"); @@ -1726,12 +1753,14 @@ WebConsoleFrame.prototype = { let duration = Math.round((end - start) * 100) / 100; let node = this.document.createElementNS(XHTML_NS, "span"); if (sourceURL) { - node.textContent = l10n.getFormatStr("reflow.messageWithLink", [duration]); + node.textContent = + l10n.getFormatStr("reflow.messageWithLink", [duration]); let a = this.document.createElementNS(XHTML_NS, "a"); a.href = "#"; a.draggable = "false"; let filename = WebConsoleUtils.abbreviateSourceURL(sourceURL); - let functionName = message.functionName || l10n.getStr("stacktrace.anonymousFunction"); + let functionName = message.functionName || + l10n.getStr("stacktrace.anonymousFunction"); a.textContent = l10n.getFormatStr("reflow.messageLinkText", [functionName, filename, sourceLine]); this._addMessageLinkCallback(a, () => { @@ -1739,12 +1768,12 @@ WebConsoleFrame.prototype = { }); node.appendChild(a); } else { - node.textContent = l10n.getFormatStr("reflow.messageWithNoLink", [duration]); + node.textContent = + l10n.getFormatStr("reflow.messageWithNoLink", [duration]); } return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node); }, - handleReflowActivity: function(message) { this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [message]); }, @@ -1816,7 +1845,8 @@ WebConsoleFrame.prototype = { let hasEventTimings = updates.indexOf("eventTimings") > -1; let hasResponseStart = updates.indexOf("responseStart") > -1; let request = networkInfo.request; - let methodText = (networkInfo.isXHR)? request.method + ' XHR' : request.method; + let methodText = (networkInfo.isXHR) ? + request.method + ' XHR' : request.method; let response = networkInfo.response; let updated = false; @@ -1995,8 +2025,8 @@ WebConsoleFrame.prototype = { // We won't bother to try to restore scroll position if this is showing // a lot of messages at once (and there are still items in the queue). // It is going to purge whatever you were looking at anyway. - let scrolledToBottom = shouldPrune || - Utils.isOutputScrolledToBottom(outputNode, scrollNode); + let scrolledToBottom = + shouldPrune || Utils.isOutputScrolledToBottom(outputNode, scrollNode); // Output the current batch of messages. let messages = new Set(); @@ -2107,7 +2137,7 @@ WebConsoleFrame.prototype = { // The last object in the args array should be message // object or response packet received from the server. - let message = (args && args.length) ? args[args.length-1] : null; + let message = (args && args.length) ? args[args.length - 1] : null; let node = typeof methodOrNode == "function" ? methodOrNode.apply(this, args || []) : @@ -2128,7 +2158,6 @@ WebConsoleFrame.prototype = { let nodeID = node.getAttribute("id"); Services.obs.notifyObservers(hudIdSupportsString, "web-console-message-created", nodeID); - } if (node._onOutput) { @@ -2220,7 +2249,8 @@ WebConsoleFrame.prototype = { } else if (typeof methodOrNode != "function") { connectionId = methodOrNode._connectionId; } - if (connectionId && this.webConsoleClient.hasNetworkRequest(connectionId)) { + if (connectionId && + this.webConsoleClient.hasNetworkRequest(connectionId)) { this.webConsoleClient.removeNetworkRequest(connectionId); this._releaseObject(connectionId); } @@ -2457,7 +2487,8 @@ WebConsoleFrame.prototype = { * message. * * @param object aLocation - * An object containing url, line and column number of the message source (destructured). + * An object containing url, line and column number of the message + * source (destructured). * @param string target [optional] * Tells which tool to open the link with, on click. Supported tools: * jsdebugger, styleeditor, scratchpad. @@ -2487,7 +2518,8 @@ WebConsoleFrame.prototype = { } filenameNode.className = "filename"; - filenameNode.textContent = " " + (filename || l10n.getStr("unknownLocation")); + filenameNode.textContent = + " " + (filename || l10n.getStr("unknownLocation")); locationNode.appendChild(filenameNode); locationNode.href = isScratchpad || !fullURL ? "#" : fullURL; @@ -2524,7 +2556,8 @@ WebConsoleFrame.prototype = { if (line) { let lineNumberNode = this.document.createElementNS(XHTML_NS, "span"); lineNumberNode.className = "line-number"; - lineNumberNode.textContent = ":" + line + (column >= 0 ? ":" + column : ""); + lineNumberNode.textContent = + ":" + line + (column >= 0 ? ":" + column : ""); locationNode.appendChild(lineNumberNode); locationNode.sourceLine = line; } @@ -2548,7 +2581,8 @@ WebConsoleFrame.prototype = { messageNode.severity = severity; messageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[category]); messageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[severity]); - messageNode.setAttribute("filter", MESSAGE_PREFERENCE_KEYS[category][severity]); + messageNode.setAttribute("filter", + MESSAGE_PREFERENCE_KEYS[category][severity]); }, /** @@ -2611,8 +2645,8 @@ WebConsoleFrame.prototype = { // If this event started with a mousedown event and it ends at a different // location, we consider this text selection. - // Add a fuzz modifier of two pixels in any direction to account for sloppy - // clicking. + // Add a fuzz modifier of two pixels in any direction to account for + // sloppy clicking. if (mousedown && (Math.abs(event.clientX - this._startX) >= 2) && (Math.abs(event.clientY - this._startY) >= 1)) { @@ -2800,7 +2834,6 @@ WebConsoleFrame.prototype = { }, }; - /** * @see VariablesView.simpleValueEvalMacro */ @@ -2808,7 +2841,6 @@ function simpleValueEvalMacro(item, currentString) { return VariablesView.simpleValueEvalMacro(item, currentString, "_self"); }; - /** * @see VariablesView.overrideValueEvalMacro */ @@ -2816,7 +2848,6 @@ function overrideValueEvalMacro(item, currentString) { return VariablesView.overrideValueEvalMacro(item, currentString, "_self"); }; - /** * @see VariablesView.getterOrSetterEvalMacro */ @@ -2824,8 +2855,6 @@ function getterOrSetterEvalMacro(item, currentString) { return VariablesView.getterOrSetterEvalMacro(item, currentString, "_self"); } - - /** * Create a JSTerminal (a JavaScript command line). This is attached to an * existing HeadsUpDisplay (a Web Console instance). This code is responsible @@ -2865,21 +2894,23 @@ JSTerm.prototype = { 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); + 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 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); + // 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); }, /** @@ -3047,9 +3078,9 @@ JSTerm.prototype = { } else { let okstring = l10n.getStr("selfxss.okstring"); let msg = l10n.getFormatStr("selfxss.msg", [okstring]); - this._onPaste = WebConsoleUtils.pasteHandlerGen(this.inputNode, - doc.getElementById("webconsole-notificationbox"), - msg, okstring); + this._onPaste = WebConsoleUtils.pasteHandlerGen( + this.inputNode, doc.getElementById("webconsole-notificationbox"), + msg, okstring); this.inputNode.addEventListener("keypress", this._keyPress, false); this.inputNode.addEventListener("paste", this._onPaste); this.inputNode.addEventListener("drop", this._onPaste); @@ -3103,7 +3134,8 @@ JSTerm.prototype = { break; case "inspectObject": this.openVariablesView({ - label: VariablesView.getString(helperResult.object, { concise: true }), + label: + VariablesView.getString(helperResult.object, { concise: true }), objectActor: helperResult.object, }); break; @@ -3240,9 +3272,9 @@ JSTerm.prototype = { * user-selected stackframe. * If you do not provide a |frame| the string will be evaluated in the * global content window. - * - selectedNodeActor: tells the NodeActor ID of the current selection in - * the Inspector, if such a selection exists. This is used by helper - * functions that can evaluate on the current selection. + * - selectedNodeActor: tells the NodeActor ID of the current selection + * in the Inspector, if such a selection exists. This is used by + * helper functions that can evaluate on the current selection. * @return object * A promise object that is resolved when the server response is * received. @@ -3429,7 +3461,7 @@ JSTerm.prototype = { if (event.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey || ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) { - return; + return; } this._sidebarDestroy(); @@ -3526,8 +3558,8 @@ JSTerm.prototype = { } else if (options.rawObject) { view._consoleLastObjectActor = null; } else { - throw new Error("Variables View cannot open without giving it an object " + - "display."); + throw new Error( + "Variables View cannot open without giving it an object display."); } expanded.then(() => { @@ -3578,8 +3610,8 @@ JSTerm.prototype = { bindObjectActor: options.objectActor.actor, }; - this.requestEvaluation("delete _self" + variableObject.symbolicName, evalOptions) - .then(onEval, onEval); + this.requestEvaluation("delete _self" + + variableObject.symbolicName, evalOptions).then(onEval, onEval); }, /** @@ -3603,13 +3635,15 @@ JSTerm.prototype = { bindObjectActor: options.objectActor.actor, }; - let newSymbolicName = variableObject.ownerView.symbolicName + '["' + newName + '"]'; + let newSymbolicName = + variableObject.ownerView.symbolicName + '["' + newName + '"]'; if (newSymbolicName == variableObject.symbolicName) { return; } - let code = "_self" + newSymbolicName + " = _self" + variableObject.symbolicName + ";" + - "delete _self" + variableObject.symbolicName; + let code = "_self" + newSymbolicName + " = _self" + + variableObject.symbolicName + ";" + "delete _self" + + variableObject.symbolicName; this.requestEvaluation(code, evalOptions).then(onEval, onEval); }, @@ -3666,7 +3700,6 @@ JSTerm.prototype = { callback && callback(response); }, - /** * Clear the Web Console output. * @@ -3801,7 +3834,7 @@ JSTerm.prototype = { let lineEndPos = inputValue.length; if (this.hasMultilineInput()) { // find index of closest newline >= cursor - for (let i = inputNode.selectionEnd; i= (this.history.length-1)) { + if (this.historyPlaceHolder >= (this.history.length - 1)) { return false; } @@ -4124,16 +4159,17 @@ JSTerm.prototype = { * - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the * value stayed the same as the last time the function was called, * then the previous completion of all possible completions is used. - * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the first - * item. - * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select the - * last item. + * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the + * first item. + * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select + * the last item. * - this.COMPLETE_HINT_ONLY: If there is more than one possible * completion and the input value stayed the same compared to the * last time this function was called, then the same completion is * used again. If there is only one possible completion, then - * the this.getInputValue() is set to this value and the selection is set - * from the current cursor position to the end of the completed text. + * the this.getInputValue() is set to this value and the selection + * is set from the current cursor position to the end of the + * completed text. * @param function callback * Optional function invoked when the autocomplete properties are * updated. @@ -4162,7 +4198,8 @@ JSTerm.prototype = { } // Update the completion results. - if (this.lastCompletion.value != inputValue || frameActor != this._lastFrameActorId) { + if (this.lastCompletion.value != inputValue || + frameActor != this._lastFrameActorId) { this._updateCompletionResult(type, callback); return false; } @@ -4254,10 +4291,11 @@ JSTerm.prototype = { value: null, }; - let autocompleteCallback = this._receiveAutocompleteProperties.bind(this, requestId, - callback); + let autocompleteCallback = + this._receiveAutocompleteProperties.bind(this, requestId, callback); - this.webConsoleClient.autocomplete(input, cursor, autocompleteCallback, frameActor); + this.webConsoleClient.autocomplete( + input, cursor, autocompleteCallback, frameActor); }, /** @@ -4280,7 +4318,8 @@ JSTerm.prototype = { requestId != this.lastCompletion.requestId) { return; } - // Cache whatever came from the server if the last char is alphanumeric or '.' + // Cache whatever came from the server if the last char is + // alphanumeric or '.' let cursor = inputNode.selectionStart; let inputUntilCursor = inputValue.substring(0, cursor); @@ -4386,7 +4425,8 @@ JSTerm.prototype = { matchProp.length); let cursor = this.inputNode.selectionStart; let value = this.getInputValue(); - this.setInputValue(value.substr(0, cursor) + suffix + value.substr(cursor)); + this.setInputValue(value.substr(0, cursor) + + suffix + value.substr(cursor)); let newCursor = cursor + suffix.length; this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor; updated = true; @@ -4409,7 +4449,6 @@ JSTerm.prototype = { this.completeNode.value = prefix + suffix; }, - /** * Destroy the sidebar. * @private @@ -4552,9 +4591,9 @@ var Utils = { }, }; -/////////////////////////////////////////////////////////////////////////////// +// //////////////////////////////////////////////////////////////////////////// // CommandController -/////////////////////////////////////////////////////////////////////////////// +// //////////////////////////////////////////////////////////////////////////// /** * A controller (an instance of nsIController) that makes editing actions @@ -4607,8 +4646,9 @@ CommandController.prototype = { return selectedItem && "url" in selectedItem; } case "cmd_copy": { - // Only copy if we right-clicked the console and there's no selected text. - // With text selected, we want to fall back onto the default copy behavior. + // Only copy if we right-clicked the console and there's no selected + // text. With text selected, we want to fall back onto the default + // copy behavior. return this.owner._contextMenuHandler.lastClickedMessage && !this.owner.output.getSelectedMessages(1)[0]; } @@ -4661,9 +4701,9 @@ CommandController.prototype = { } }; -/////////////////////////////////////////////////////////////////////////////// +// //////////////////////////////////////////////////////////////////////////// // Web Console connection proxy -/////////////////////////////////////////////////////////////////////////////// +// //////////////////////////////////////////////////////////////////////////// /** * The WebConsoleConnectionProxy handles the connection between the Web Console @@ -4691,7 +4731,8 @@ function WebConsoleConnectionProxy(webConsoleFrame, target) { this._onAttachConsole = this._onAttachConsole.bind(this); this._onCachedMessages = this._onCachedMessages.bind(this); this._connectionTimeout = this._connectionTimeout.bind(this); - this._onLastPrivateContextExited = this._onLastPrivateContextExited.bind(this); + this._onLastPrivateContextExited = + this._onLastPrivateContextExited.bind(this); } WebConsoleConnectionProxy.prototype = { @@ -4795,7 +4836,8 @@ WebConsoleConnectionProxy.prototype = { client.addListener("fileActivity", this._onFileActivity); client.addListener("reflowActivity", this._onReflowActivity); client.addListener("serverLogCall", this._onServerLogCall); - client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited); + client.addListener("lastPrivateContextExited", + this._onLastPrivateContextExited); } this.target.on("will-navigate", this._onTabNavigated); this.target.on("navigate", this._onTabNavigated); @@ -4890,7 +4932,8 @@ WebConsoleConnectionProxy.prototype = { Cu.reportError("Web Console getCachedMessages error: invalid state."); } - let messages = response.messages.concat(...this.webConsoleClient.getNetworkEvents()); + let messages = + response.messages.concat(...this.webConsoleClient.getNetworkEvents()); messages.sort((a, b) => a.timeStamp - b.timeStamp); this.webConsoleFrame.displayCachedMessages(messages); @@ -5093,7 +5136,8 @@ WebConsoleConnectionProxy.prototype = { this.client.removeListener("fileActivity", this._onFileActivity); this.client.removeListener("reflowActivity", this._onReflowActivity); this.client.removeListener("serverLogCall", this._onServerLogCall); - this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited); + this.client.removeListener("lastPrivateContextExited", + this._onLastPrivateContextExited); this.webConsoleClient.off("networkEvent", this._onNetworkEvent); this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate); this.target.off("will-navigate", this._onTabNavigated); @@ -5115,9 +5159,9 @@ function gSequenceId() { } gSequenceId.n = 0; -/////////////////////////////////////////////////////////////////////////////// +// //////////////////////////////////////////////////////////////////////////// // Context Menu -/////////////////////////////////////////////////////////////////////////////// +// //////////////////////////////////////////////////////////////////////////// /* * ConsoleContextMenu this used to handle the visibility of context menu items. diff --git a/devtools/server/tests/unit/test_objectgrips-12.js b/devtools/server/tests/unit/test_objectgrips-12.js index e0c81523fb7a..307ce54ce284 100644 --- a/devtools/server/tests/unit/test_objectgrips-12.js +++ b/devtools/server/tests/unit/test_objectgrips-12.js @@ -3,6 +3,8 @@ // Test getDisplayString. +Cu.import("resource://testing-common/PromiseTestUtils.jsm", this); + var gDebuggee; var gClient; var gThreadClient; @@ -125,6 +127,7 @@ function test_display_string() output: "Promise (fulfilled: 5)" }, { + // This rejection is left uncaught, see expectUncaughtRejection below. input: "Promise.reject(new Error())", output: "Promise (rejected: Error)" }, @@ -134,6 +137,8 @@ function test_display_string() } ]; + PromiseTestUtils.expectUncaughtRejection(/Error/); + gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) { const args = aPacket.frame.arguments; diff --git a/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js b/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js index 970e171a696d..77012b1da4b2 100644 --- a/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js +++ b/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js @@ -8,6 +8,8 @@ "use strict"; +Cu.import("resource://testing-common/PromiseTestUtils.jsm", this); + const { PromisesFront } = require("devtools/server/actors/promises"); var events = require("sdk/event/core"); @@ -52,6 +54,7 @@ function* testPromisesSettled(client, form, makeResolvePromise, let foundResolvedPromise = yield onPromiseSettled; ok(foundResolvedPromise, "Found our resolved promise"); + PromiseTestUtils.expectUncaughtRejection(r => r.message == resolution); onPromiseSettled = oncePromiseSettled(front, resolution, false, true); let rejectedPromise = makeRejectPromise(resolution); let foundRejectedPromise = yield onPromiseSettled; diff --git a/devtools/shared/acorn/tests/unit/head_acorn.js b/devtools/shared/acorn/tests/unit/head_acorn.js index 5cddb4355a9b..3e06ca3c1a90 100644 --- a/devtools/shared/acorn/tests/unit/head_acorn.js +++ b/devtools/shared/acorn/tests/unit/head_acorn.js @@ -62,6 +62,11 @@ var listener = { } } + // Ignored until they are fixed in bug 1242968. + if (string.includes("JavaScript Warning")) { + return; + } + do_throw("head_acorn.js got console message: " + string + "\n"); } }; diff --git a/devtools/shared/pretty-fast/tests/unit/head_pretty-fast.js b/devtools/shared/pretty-fast/tests/unit/head_pretty-fast.js index af999f4deff2..abde4b197e57 100644 --- a/devtools/shared/pretty-fast/tests/unit/head_pretty-fast.js +++ b/devtools/shared/pretty-fast/tests/unit/head_pretty-fast.js @@ -35,6 +35,11 @@ var listener = { } } + // Ignored until they are fixed in bug 1242968. + if (string.includes("JavaScript Warning")) { + return; + } + do_throw("head_pretty-fast.js got console message: " + string + "\n"); } }; diff --git a/dom/events/IMEStateManager.cpp b/dom/events/IMEStateManager.cpp index e782eb352a1c..c69376d60a07 100644 --- a/dom/events/IMEStateManager.cpp +++ b/dom/events/IMEStateManager.cpp @@ -1034,7 +1034,8 @@ IMEStateManager::SetIMEState(const IMEState& aState, context.mHTMLInputType.Assign(nsGkAtoms::textarea->GetUTF16String()); } - if (Preferences::GetBool("dom.forms.inputmode", false)) { + if (Preferences::GetBool("dom.forms.inputmode", false) || + nsContentUtils::IsChromeDoc(aContent->OwnerDoc())) { aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::inputmode, context.mHTMLInputInputmode); } diff --git a/dom/promise/tests/unit/test_monitor_uncaught.js b/dom/promise/tests/unit/test_monitor_uncaught.js index f42bef52596b..7dd80d212c1d 100644 --- a/dom/promise/tests/unit/test_monitor_uncaught.js +++ b/dom/promise/tests/unit/test_monitor_uncaught.js @@ -7,6 +7,10 @@ var { utils: Cu } = Components; Cu.import("resource://gre/modules/Timer.jsm", this); +Cu.import("resource://testing-common/PromiseTestUtils.jsm", this); + +// Prevent test failures due to the unhandled rejections in this test file. +PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest(); add_task(function* test_globals() { Assert.equal(Promise.defer || undefined, undefined, "We are testing DOM Promise."); diff --git a/dom/push/PushService.jsm b/dom/push/PushService.jsm index 486c7d7be983..ddb7b2491d99 100644 --- a/dom/push/PushService.jsm +++ b/dom/push/PushService.jsm @@ -458,7 +458,7 @@ this.PushService = { // Before completing the activation check prefs. This will first check // connection.enabled pref and then check offline state. this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled")); - }); + }).catch(Cu.reportError); } else { // This is only used for testing. Different tests require connecting to diff --git a/dom/push/PushServiceHttp2.jsm b/dom/push/PushServiceHttp2.jsm index efeedc80c82d..7e2048c53f50 100644 --- a/dom/push/PushServiceHttp2.jsm +++ b/dom/push/PushServiceHttp2.jsm @@ -726,12 +726,15 @@ this.PushServiceHttp2 = { .then(record => this._subscribeResource(record) .then(recordNew => { if (this._mainPushService) { - this._mainPushService.updateRegistrationAndNotifyApp(aSubscriptionUri, - recordNew); + this._mainPushService + .updateRegistrationAndNotifyApp(aSubscriptionUri, recordNew) + .catch(Cu.reportError); } }, error => { if (this._mainPushService) { - this._mainPushService.dropRegistrationAndNotifyApp(aSubscriptionUri); + this._mainPushService + .dropRegistrationAndNotifyApp(aSubscriptionUri) + .catch(Cu.reportError); } }) ); diff --git a/dom/push/test/xpcshell/test_registration_success_http2.js b/dom/push/test/xpcshell/test_registration_success_http2.js index e7d3c897f3ab..f955ba2688e6 100644 --- a/dom/push/test/xpcshell/test_registration_success_http2.js +++ b/dom/push/test/xpcshell/test_registration_success_http2.js @@ -4,6 +4,15 @@ 'use strict'; Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://testing-common/PromiseTestUtils.jsm"); + +/////////////////// +// +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// +// Instances of the rejection "record is undefined" may or may not appear. +PromiseTestUtils.thisTestLeaksUncaughtRejectionsAndShouldBeFixed(); const {PushDB, PushService, PushServiceHttp2} = serviceExports; diff --git a/dom/push/test/xpcshell/test_unregister_success_http2.js b/dom/push/test/xpcshell/test_unregister_success_http2.js index b6c77ca3d6d0..c7c63f829337 100644 --- a/dom/push/test/xpcshell/test_unregister_success_http2.js +++ b/dom/push/test/xpcshell/test_unregister_success_http2.js @@ -4,6 +4,15 @@ 'use strict'; Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://testing-common/PromiseTestUtils.jsm"); + +/////////////////// +// +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// +// Instances of the rejection "record is undefined" may or may not appear. +PromiseTestUtils.thisTestLeaksUncaughtRejectionsAndShouldBeFixed(); const {PushDB, PushService, PushServiceHttp2} = serviceExports; diff --git a/js/xpconnect/tests/unit/head_watchdog.js b/js/xpconnect/tests/unit/head_watchdog.js index e091c02b6058..cf006303aae8 100644 --- a/js/xpconnect/tests/unit/head_watchdog.js +++ b/js/xpconnect/tests/unit/head_watchdog.js @@ -10,6 +10,17 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +Cu.import("resource://testing-common/PromiseTestUtils.jsm"); + +/////////////////// +// +// Whitelisting these tests. +// As part of bug 1077403, the shutdown crash should be fixed. +// +// These tests may crash intermittently on shutdown if the DOM Promise uncaught +// rejection observers are still registered when the watchdog operates. +PromiseTestUtils.thisTestLeaksUncaughtRejectionsAndShouldBeFixed(); + var gPrefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); function setWatchdogEnabled(enabled) { diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index c791a8d254c4..f96990cd2c5c 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -635,8 +635,8 @@ pref("image.downscale-during-decode.enabled", true); #ifdef MOZ_SAFE_BROWSING pref("browser.safebrowsing.enabled", true); pref("browser.safebrowsing.malware.enabled", true); -pref("browser.safebrowsing.downloads.enabled", true); -pref("browser.safebrowsing.downloads.remote.enabled", true); +pref("browser.safebrowsing.downloads.enabled", false); +pref("browser.safebrowsing.downloads.remote.enabled", false); pref("browser.safebrowsing.downloads.remote.timeout_ms", 10000); pref("browser.safebrowsing.downloads.remote.url", "https://sb-ssl.google.com/safebrowsing/clientreport/download?key=%GOOGLE_API_KEY%"); pref("browser.safebrowsing.downloads.remote.block_dangerous", true); diff --git a/mobile/android/base/android-services.mozbuild b/mobile/android/base/android-services.mozbuild index 48d590149512..b45c6c33ead8 100644 --- a/mobile/android/base/android-services.mozbuild +++ b/mobile/android/base/android-services.mozbuild @@ -859,6 +859,10 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil 'fxa/sync/FxAccountSyncStatusHelper.java', 'fxa/sync/SchedulePolicy.java', 'fxa/SyncStatusListener.java', + 'push/autopush/AutopushClient.java', + 'push/autopush/AutopushClientException.java', + 'push/RegisterUserAgentResponse.java', + 'push/SubscribeChannelResponse.java', 'sync/AlreadySyncingException.java', 'sync/BackoffHandler.java', 'sync/BadRequiredFieldJSONException.java', diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java index 37e09fa2c5fd..69190d3bdecc 100644 --- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java +++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java @@ -8,6 +8,7 @@ package org.mozilla.gecko; import android.Manifest; import android.os.AsyncTask; import android.support.annotation.NonNull; +import org.json.JSONArray; import org.mozilla.gecko.adjust.AdjustHelperInterface; import org.mozilla.gecko.annotation.RobocopTarget; import org.mozilla.gecko.AppConstants.Versions; @@ -682,6 +683,7 @@ public class BrowserApp extends GeckoApp EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this, "CharEncoding:Data", "CharEncoding:State", + "Experiments:GetActive", "Favicon:CacheLoad", "Feedback:LastUrl", "Feedback:MaybeLater", @@ -1163,7 +1165,12 @@ public class BrowserApp extends GeckoApp final String title = tab.getDisplayTitle(); if (url != null && title != null) { - GeckoAppShell.createShortcut(title, url); + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + GeckoAppShell.createShortcut(title, url); + } + }); } } } @@ -1299,13 +1306,13 @@ public class BrowserApp extends GeckoApp return true; } - new AsyncTask() { + ThreadUtils.postToBackgroundThread(new Runnable() { @Override - protected Void doInBackground(Void... voids) { + public void run() { GeckoAppShell.createShortcut(title, url); - return null; + } - }.execute(); + }); Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, getResources().getResourceEntryName(itemId)); @@ -1377,6 +1384,7 @@ public class BrowserApp extends GeckoApp EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this, "CharEncoding:Data", "CharEncoding:State", + "Experiments:GetActive", "Favicon:CacheLoad", "Feedback:LastUrl", "Feedback:MaybeLater", @@ -1659,6 +1667,10 @@ public class BrowserApp extends GeckoApp } }); + } else if ("Experiments:GetActive".equals(event)) { + final List experiments = SwitchBoard.getActiveExperiments(this); + final JSONArray json = new JSONArray(experiments); + callback.sendSuccess(json.toString()); } else if ("Favicon:CacheLoad".equals(event)) { final String url = message.getString("url"); getFaviconFromCache(callback, url); @@ -3924,7 +3936,7 @@ public class BrowserApp extends GeckoApp } private void uploadTelemetry(final GeckoProfile profile) { - if (!TelemetryConstants.UPLOAD_ENABLED || profile.inGuestMode()) { + if (!TelemetryUploadService.isUploadEnabledByProfileConfig(this, profile)) { return; } diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java index d8c617a9cb27..7603fe570915 100644 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java +++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java @@ -221,13 +221,14 @@ public abstract class HomeFragment extends Fragment { // Fetch an icon big enough for use as a home screen icon. final String displayTitle = info.getDisplayTitle(); - new AsyncTask() { + ThreadUtils.postToBackgroundThread(new Runnable() { @Override - protected Void doInBackground(Void... voids) { + public void run() { GeckoAppShell.createShortcut(displayTitle, info.url); - return null; + } - }.execute(); + }); + return true; } diff --git a/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java b/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java index b542be6f3100..7236cb97bef0 100644 --- a/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java +++ b/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java @@ -484,7 +484,7 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog * is closing. */ - private void notifyClosing(final JSONObject aReturn) { + private void notifyClosing(JSONObject aReturn) { try { aReturn.put("guid", mGuid); } catch(JSONException ex) { } @@ -497,12 +497,7 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent()); if (mCallback != null) { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - mCallback.onPromptFinished(aReturn.toString()); - } - }); + mCallback.onPromptFinished(aReturn.toString()); } } @@ -551,6 +546,11 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis } public interface PromptCallback { + + /** + * Called when the Prompt has been completed (i.e. when the user has selected an item or action in the Prompt). + * This callback is run on the UI thread. + */ public void onPromptFinished(String jsonResult); } } diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java index 1c9552cefd97..6f16d27396be 100644 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java +++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java @@ -4,6 +4,7 @@ package org.mozilla.gecko.telemetry; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.support.annotation.NonNull; @@ -13,6 +14,7 @@ import ch.boye.httpclientandroidlib.client.ClientProtocolException; import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.background.BackgroundService; +import org.mozilla.gecko.preferences.GeckoPreferences; import org.mozilla.gecko.sync.net.BaseResource; import org.mozilla.gecko.sync.net.BaseResourceDelegate; import org.mozilla.gecko.sync.net.Resource; @@ -59,12 +61,15 @@ public class TelemetryUploadService extends BackgroundService { public void onHandleIntent(final Intent intent) { Log.d(LOGTAG, "Service started"); - if (!TelemetryConstants.UPLOAD_ENABLED) { - Log.d(LOGTAG, "Telemetry upload feature is compile-time disabled; not handling upload intent."); + // Sanity check: is upload enabled? Generally, the caller should check this before starting the service. + // Since we don't have the profile here, we rely on the caller to check the enabled state for the profile. + if (!isUploadEnabledByAppConfig(this)) { + Log.w(LOGTAG, "Upload is not available by configuration; returning"); return; } - if (!isReadyToUpload(intent)) { + if (!isIntentValid(intent)) { + Log.w(LOGTAG, "Received invalid Intent; returning"); return; } @@ -82,38 +87,73 @@ public class TelemetryUploadService extends BackgroundService { uploadCorePing(docId, seq, profileName, profilePath); } - private boolean isReadyToUpload(final Intent intent) { - // Intent can be null. Bug 1025937. - if (intent == null) { - Log.d(LOGTAG, "Received null intent. Returning."); + /** + * Determines if the telemetry upload feature is enabled via the application configuration. Prefer to use + * {@link #isUploadEnabledByProfileConfig(Context, GeckoProfile)} if the profile is available as it takes into + * account more information. + * + * Note that this method logs debug statements when upload is disabled. + */ + public static boolean isUploadEnabledByAppConfig(final Context context) { + if (!TelemetryConstants.UPLOAD_ENABLED) { + Log.d(LOGTAG, "Telemetry upload feature is compile-time disabled"); return false; } - // Don't do anything if the device can't talk to the server. - if (!backgroundDataIsEnabled()) { - Log.d(LOGTAG, "Background data is not enabled; skipping."); + if (!GeckoPreferences.getBooleanPref(context, GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true)) { + Log.d(LOGTAG, "Telemetry upload opt-out"); + return false; + } + + if (!backgroundDataIsEnabled(context)) { + Log.d(LOGTAG, "Background data is disabled"); + return false; + } + + return true; + } + + /** + * Determines if the telemetry upload feature is enabled via profile & application level configurations. This is the + * preferred method. + * + * Note that this method logs debug statements when upload is disabled. + */ + public static boolean isUploadEnabledByProfileConfig(final Context context, final GeckoProfile profile) { + if (profile.inGuestMode()) { + Log.d(LOGTAG, "Profile is in guest mode"); + return false; + } + + return isUploadEnabledByAppConfig(context); + } + + private boolean isIntentValid(final Intent intent) { + // Intent can be null. Bug 1025937. + if (intent == null) { + Log.d(LOGTAG, "Received null intent"); return false; } if (intent.getStringExtra(TelemetryConstants.EXTRA_DOC_ID) == null) { - Log.w(LOGTAG, "Received invalid doc ID in Intent. Returning"); + Log.d(LOGTAG, "Received invalid doc ID in Intent"); return false; } if (!intent.hasExtra(TelemetryConstants.EXTRA_SEQ)) { - Log.w(LOGTAG, "Received Intent without sequence number. Returning"); + Log.d(LOGTAG, "Received Intent without sequence number"); return false; } if (intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_NAME) == null) { - Log.w(LOGTAG, "Received invalid profile name in Intent. Returning"); + Log.d(LOGTAG, "Received invalid profile name in Intent"); return false; } // GeckoProfile can use the name to get the path so this isn't strictly necessary. // However, getting the path requires parsing an ini file so we optimize by including it here. if (intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_PATH) == null) { - Log.w(LOGTAG, "Received invalid profile path in Intent. Returning"); + Log.d(LOGTAG, "Received invalid profile path in Intent"); return false; } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index aa1156ff523d..be7b3ec47f21 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -503,6 +503,7 @@ var BrowserApp = { Distribution.init(); Tabs.init(); SearchEngines.init(); + Experiments.init(); if ("arguments" in window) { if (window.arguments[0]) @@ -6822,6 +6823,33 @@ var Telemetry = { }, }; +var Experiments = { + + // Enable malware download protection (bug 936041) + MALWARE_DOWNLOAD_PROTECTION: "malware-download-protection", + + init() { + Messaging.sendRequestForResult({ + type: "Experiments:GetActive" + }).then(experiments => { + let names = JSON.parse(experiments); + for (let name of names) { + switch (name) { + case this.MALWARE_DOWNLOAD_PROTECTION: { + // Apply experiment preferences on the default branch. This allows + // us to avoid migrating user prefs when experiments are enabled/disabled, + // and it also allows users to override these prefs in about:config. + let defaults = Services.prefs.getDefaultBranch(null); + defaults.setBoolPref("browser.safebrowsing.downloads.enabled", true); + defaults.setBoolPref("browser.safebrowsing.downloads.remote.enabled", true); + continue; + } + } + } + }); + } +}; + var ExternalApps = { _contextMenuId: null, diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/BackgroundService.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/BackgroundService.java index e3c9aa1c1132..dbc5d3901c3b 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/BackgroundService.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/BackgroundService.java @@ -41,8 +41,8 @@ public abstract class BackgroundService extends IntentService { * data operations. This logic varies by OS version. */ @SuppressWarnings("deprecation") - protected boolean backgroundDataIsEnabled() { - ConnectivityManager connectivity = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE); + protected static boolean backgroundDataIsEnabled(final Context context) { + final ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { return connectivity.getBackgroundDataSetting(); } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java new file mode 100644 index 000000000000..3bbb7e8b42ce --- /dev/null +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java @@ -0,0 +1,19 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.push; + +/** + * Thin container for a register User-Agent response. + */ +public class RegisterUserAgentResponse { + public final String uaid; + public final String secret; + + public RegisterUserAgentResponse(String uaid, String secret) { + this.uaid = uaid; + this.secret = secret; + } +} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java new file mode 100644 index 000000000000..009a7f83882c --- /dev/null +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java @@ -0,0 +1,19 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.push; + +/** + * Thin container for a subscribe channel response. + */ +public class SubscribeChannelResponse { + public final String channelID; + public final String endpoint; + + public SubscribeChannelResponse(String channelID, String endpoint) { + this.channelID = channelID; + this.endpoint = endpoint; + } +} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java new file mode 100644 index 000000000000..4d33f537d6f8 --- /dev/null +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java @@ -0,0 +1,405 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.push.autopush; + +import android.text.TextUtils; + +import org.mozilla.gecko.Locales; +import org.mozilla.gecko.fxa.FxAccountConstants; +import org.mozilla.gecko.push.RegisterUserAgentResponse; +import org.mozilla.gecko.push.SubscribeChannelResponse; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BaseResource; +import org.mozilla.gecko.sync.net.BaseResourceDelegate; +import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider; +import org.mozilla.gecko.sync.net.Resource; +import org.mozilla.gecko.sync.net.SyncResponse; +import org.mozilla.gecko.sync.net.SyncStorageResponse; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.util.Locale; +import java.util.concurrent.Executor; + +import ch.boye.httpclientandroidlib.HttpEntity; +import ch.boye.httpclientandroidlib.HttpHeaders; +import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.client.ClientProtocolException; +import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; +import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; + + +/** + * Interact with the autopush endpoint HTTP API. + *

+ * The API is a Mozilla-proprietary interface, and not even specified to Mozilla's usual ad-hoc standards. + * This client is written against a work-in-progress, un-deployed upstream commit. + */ +public class AutopushClient { + protected static final String LOG_TAG = AutopushClient.class.getSimpleName(); + + protected static final String ACCEPT_HEADER = "application/json;charset=utf-8"; + protected static final String TYPE = "gcm"; + + protected static final String JSON_KEY_UAID = "uaid"; + protected static final String JSON_KEY_SECRET = "secret"; + protected static final String JSON_KEY_CHANNEL_ID = "channelID"; + protected static final String JSON_KEY_ENDPOINT = "endpoint"; + + protected static final String[] REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UAID, JSON_KEY_SECRET, JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT }; + protected static final String[] REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT }; + + public static final String JSON_KEY_CODE = "code"; + public static final String JSON_KEY_ERRNO = "errno"; + public static final String JSON_KEY_ERROR = "error"; + public static final String JSON_KEY_MESSAGE = "message"; + + /** + * The server's URI. + *

+ * We assume throughout that this ends with a trailing slash (and guarantee as + * much in the constructor). + */ + public final String serverURI; + + protected final Executor executor; + + public AutopushClient(String serverURI, Executor executor) { + if (serverURI == null) { + throw new IllegalArgumentException("Must provide a server URI."); + } + if (executor == null) { + throw new IllegalArgumentException("Must provide a non-null executor."); + } + this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/"; + if (!this.serverURI.endsWith("/")) { + throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI); + } + this.executor = executor; + } + + /** + * A legal autopush server URL includes a sender ID embedded into it. Extract it. + * + * @return a non-null non-empty sender ID. + * @throws AutopushClientException on failure. + */ + public String getSenderIDFromServerURI() throws AutopushClientException { + // Turn "https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407/" into "829133274407". + final String[] parts = serverURI.split("/", -1); // The -1 keeps the trailing empty part. + if (parts.length < 3) { + throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI); + } + if (!TextUtils.isEmpty(parts[parts.length - 1])) { + // We guarantee a trailing slash, so we should always have an empty part at the tail. + throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI); + } + if (!TextUtils.equals("gcm", parts[parts.length - 3])) { + // We should always have /gcm/senderID/. + throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI); + } + final String senderID = parts[parts.length - 2]; + if (TextUtils.isEmpty(senderID)) { + // Something is horribly wrong -- we have /gcm//. Abort. + throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI); + } + return senderID; + } + + /** + * Process a typed value extracted from a successful response (in an + * endpoint-dependent way). + */ + public interface RequestDelegate { + void handleError(Exception e); + void handleFailure(AutopushClientException e); + void handleSuccess(T result); + } + + /** + * Intepret a response from the autopush server. + *

+ * Throw an appropriate exception on errors; otherwise, return the response's + * status code. + * + * @return response's HTTP status code. + * @throws AutopushClientException + */ + public static int validateResponse(HttpResponse response) throws AutopushClientException { + final int status = response.getStatusLine().getStatusCode(); + if (200 <= status && status <= 299) { + return status; + } + int code; + int errno; + String error; + String message; + String info; + ExtendedJSONObject body; + try { + body = new SyncStorageResponse(response).jsonObjectBody(); + // TODO: The service doesn't do the right thing yet :( + // body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class); + // body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class); + code = body.getLong(JSON_KEY_CODE).intValue(); + errno = body.getLong(JSON_KEY_ERRNO).intValue(); + error = body.getString(JSON_KEY_ERROR); + message = body.getString(JSON_KEY_MESSAGE); + } catch (Exception e) { + throw new AutopushClientException.AutopushClientMalformedResponseException(response); + } + throw new AutopushClientException.AutopushClientRemoteException(response, code, errno, error, message, body); + } + + protected void invokeHandleError(final RequestDelegate delegate, final Exception e) { + executor.execute(new Runnable() { + @Override + public void run() { + delegate.handleError(e); + } + }); + } + + protected void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate delegate) { + try { + if (requestBody == null) { + resource.post((HttpEntity) null); + } else { + resource.post(requestBody); + } + } catch (Exception e) { + invokeHandleError(delegate, e); + return; + } + } + + /** + * Translate resource callbacks into request callbacks invoked on the provided + * executor. + *

+ * Override handleSuccess to parse the body of the resource + * request and call the request callback. handleSuccess is + * invoked via the executor, so you don't need to delegate further. + */ + protected abstract class ResourceDelegate extends BaseResourceDelegate { + protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body); + + protected final String secret; + protected final RequestDelegate delegate; + + /** + * Create a delegate for an un-authenticated resource. + */ + public ResourceDelegate(final Resource resource, final String secret, final RequestDelegate delegate) { + super(resource); + this.delegate = delegate; + this.secret = secret; + } + + @Override + public AuthHeaderProvider getAuthHeaderProvider() { + if (secret != null) { + return new BearerAuthHeaderProvider(secret); + } + return null; + } + + @Override + public String getUserAgent() { + return FxAccountConstants.USER_AGENT; + } + + @Override + public void handleHttpResponse(HttpResponse response) { + try { + final int status = validateResponse(response); + invokeHandleSuccess(status, response); + } catch (AutopushClientException e) { + invokeHandleFailure(e); + } + } + + protected void invokeHandleFailure(final AutopushClientException e) { + executor.execute(new Runnable() { + @Override + public void run() { + delegate.handleFailure(e); + } + }); + } + + protected void invokeHandleSuccess(final int status, final HttpResponse response) { + executor.execute(new Runnable() { + @Override + public void run() { + try { + ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody(); + ResourceDelegate.this.handleSuccess(status, response, body); + } catch (Exception e) { + delegate.handleError(e); + } + } + }); + } + + @Override + public void handleHttpProtocolException(final ClientProtocolException e) { + invokeHandleError(delegate, e); + } + + @Override + public void handleHttpIOException(IOException e) { + invokeHandleError(delegate, e); + } + + @Override + public void handleTransportException(GeneralSecurityException e) { + invokeHandleError(delegate, e); + } + + @Override + public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { + super.addHeaders(request, client); + + // The basics. + final Locale locale = Locale.getDefault(); + request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale)); + request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER); + } + } + + public void registerUserAgent(final String token, RequestDelegate delegate) { + BaseResource resource; + try { + resource = new BaseResource(new URI(serverURI + "registration")); + } catch (URISyntaxException e) { + invokeHandleError(delegate, e); + return; + } + + resource.delegate = new ResourceDelegate(resource, null, delegate) { + @Override + public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { + try { + body.throwIfFieldsMissingOrMisTyped(REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS, String.class); + final String uaid = body.getString(JSON_KEY_UAID); + final String secret = body.getString(JSON_KEY_SECRET); + delegate.handleSuccess(new RegisterUserAgentResponse(uaid, secret)); + return; + } catch (Exception e) { + delegate.handleError(e); + return; + } + } + }; + + final ExtendedJSONObject body = new ExtendedJSONObject(); + body.put("type", TYPE); + body.put("token", token); + + resource.post(body); + } + + public void reregisterUserAgent(final String uaid, final String secret, final String token, RequestDelegate delegate) { + final BaseResource resource; + try { + resource = new BaseResource(new URI(serverURI + "registration/" + uaid)); + } catch (Exception e) { + invokeHandleError(delegate, e); + return; + } + + resource.delegate = new ResourceDelegate(resource, secret, delegate) { + @Override + public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { + try { + delegate.handleSuccess(null); + return; + } catch (Exception e) { + delegate.handleError(e); + return; + } + } + }; + + final ExtendedJSONObject body = new ExtendedJSONObject(); + body.put("type", TYPE); + body.put("token", token); + + resource.put(body); + } + + + public void subscribeChannel(final String uaid, final String secret, RequestDelegate delegate) { + final BaseResource resource; + try { + resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription")); + } catch (Exception e) { + invokeHandleError(delegate, e); + return; + } + + resource.delegate = new ResourceDelegate(resource, secret, delegate) { + @Override + public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { + try { + body.throwIfFieldsMissingOrMisTyped(REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS, String.class); + final String channelID = body.getString(JSON_KEY_CHANNEL_ID); + final String endpoint = body.getString(JSON_KEY_ENDPOINT); + delegate.handleSuccess(new SubscribeChannelResponse(channelID, endpoint)); + return; + } catch (Exception e) { + delegate.handleError(e); + return; + } + } + }; + + final ExtendedJSONObject body = new ExtendedJSONObject(); + resource.post(body); + } + + public void unsubscribeChannel(final String uaid, final String secret, final String channelID, RequestDelegate delegate) { + final BaseResource resource; + try { + resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription/" + channelID)); + } catch (Exception e) { + invokeHandleError(delegate, e); + return; + } + + resource.delegate = new ResourceDelegate(resource, secret, delegate) { + @Override + public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { + delegate.handleSuccess(null); + } + }; + + resource.delete(); + } + + public void unregisterUserAgent(final String uaid, final String secret, RequestDelegate delegate) { + final BaseResource resource; + try { + resource = new BaseResource(new URI(serverURI + "registration/" + uaid)); + } catch (Exception e) { + invokeHandleError(delegate, e); + return; + } + + resource.delegate = new ResourceDelegate(resource, secret, delegate) { + @Override + public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { + delegate.handleSuccess(null); + } + }; + + resource.delete(); + } +} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java new file mode 100644 index 000000000000..1c5161d824e0 --- /dev/null +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.push.autopush; + +import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.HttpStatus; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.HTTPFailureException; +import org.mozilla.gecko.sync.net.SyncStorageResponse; + +public class AutopushClientException extends Exception { + private static final long serialVersionUID = 7953459541558266500L; + + public AutopushClientException(String detailMessage) { + super(detailMessage); + } + + public AutopushClientException(Exception e) { + super(e); + } + + public static class AutopushClientRemoteException extends AutopushClientException { + private static final long serialVersionUID = 2209313149952001000L; + + public final HttpResponse response; + public final long httpStatusCode; + public final long apiErrorNumber; + public final String error; + public final String message; + public final ExtendedJSONObject body; + + public AutopushClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, ExtendedJSONObject body) { + super(new HTTPFailureException(new SyncStorageResponse(response))); + if (body == null) { + throw new IllegalArgumentException("body must not be null"); + } + this.response = response; + this.httpStatusCode = httpStatusCode; + this.apiErrorNumber = apiErrorNumber; + this.error = error; + this.message = message; + this.body = body; + } + + @Override + public String toString() { + return ""; + } + + public boolean isInvalidAuthentication() { + return httpStatusCode == HttpStatus.SC_UNAUTHORIZED; + } + + public boolean isNotFound() { + return httpStatusCode == HttpStatus.SC_NOT_FOUND; + } + } + + public static class AutopushClientMalformedResponseException extends AutopushClientRemoteException { + private static final long serialVersionUID = 2209313149952001909L; + + public AutopushClientMalformedResponseException(HttpResponse response) { + super(response, 0, 999, "Response malformed", "Response malformed", new ExtendedJSONObject()); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java index f50bf8fa6a75..00c2d2188092 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java @@ -7,6 +7,7 @@ import org.mozilla.gecko.background.common.log.Logger; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; /** @@ -168,4 +169,14 @@ public class WaitHelper { public boolean isIdle() { return queue.isEmpty(); } + + public static Executor newSynchronousExecutor() { + return new Executor() { + + @Override + public void execute(Runnable runnable) { + runnable.run(); + } + }; + } } diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java new file mode 100644 index 000000000000..7f2277c63339 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.push.autopush.test; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.push.autopush.AutopushClient; +import org.mozilla.gecko.push.autopush.AutopushClientException; + +@RunWith(TestRunner.class) +public class TestAutopushClient { + @Test + public void testGetSenderID() throws Exception { + final AutopushClient client = new AutopushClient("https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407", + WaitHelper.newSynchronousExecutor()); + Assert.assertEquals("829133274407", client.getSenderIDFromServerURI()); + } + + @Test(expected=AutopushClientException.class) + public void testGetNoSenderID() throws Exception { + final AutopushClient client = new AutopushClient("https://updates-autopush-dev.stage.mozaws.net/v1/gcm", + WaitHelper.newSynchronousExecutor()); + client.getSenderIDFromServerURI(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java new file mode 100644 index 000000000000..dce3703c4e92 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java @@ -0,0 +1,141 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.push.autopush.test; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mozilla.gecko.background.fxa.FxAccountUtils; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.push.RegisterUserAgentResponse; +import org.mozilla.gecko.push.SubscribeChannelResponse; +import org.mozilla.gecko.push.autopush.AutopushClient; +import org.mozilla.gecko.push.autopush.AutopushClient.RequestDelegate; +import org.mozilla.gecko.push.autopush.AutopushClientException; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.net.BaseResource; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * This test straddles an awkward line: it uses Mockito, but doesn't actually mock the service + * endpoint. That's why it's a live test: most of its value is checking that the client + * implementation and the upstream server implementation are corresponding correctly. + */ +@RunWith(TestRunner.class) +@Ignore("Live test that requires network connection -- remove this line to run this test.") +public class TestLiveAutopushClient { + final String serverURL = "https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407"; + + protected AutopushClient client; + + @Before + public void setUp() throws Exception { + BaseResource.rewriteLocalhost = false; + client = new AutopushClient(serverURL, WaitHelper.newSynchronousExecutor()); + } + + protected T assertSuccess(RequestDelegate delegate, Class klass) { + verify(delegate, never()).handleError(any(Exception.class)); + verify(delegate, never()).handleFailure(any(AutopushClientException.class)); + + final ArgumentCaptor register = ArgumentCaptor.forClass(klass); + verify(delegate).handleSuccess(register.capture()); + + return register.getValue(); + } + + protected AutopushClientException assertFailure(RequestDelegate delegate, Class klass) { + verify(delegate, never()).handleError(any(Exception.class)); + verify(delegate, never()).handleSuccess(any(klass)); + + final ArgumentCaptor failure = ArgumentCaptor.forClass(AutopushClientException.class); + verify(delegate).handleFailure(failure.capture()); + + return failure.getValue(); + } + + @Test + public void testUserAgent() throws Exception { + final RequestDelegate registerDelegate = mock(RequestDelegate.class); + client.registerUserAgent(Utils.generateGuid(), registerDelegate); + + final RegisterUserAgentResponse registerResponse = assertSuccess(registerDelegate, RegisterUserAgentResponse.class); + Assert.assertNotNull(registerResponse); + Assert.assertNotNull(registerResponse.uaid); + Assert.assertNotNull(registerResponse.secret); + + // Reregistering with a new GUID should succeed. + final RequestDelegate reregisterDelegate = mock(RequestDelegate.class); + client.reregisterUserAgent(registerResponse.uaid, registerResponse.secret, Utils.generateGuid(), reregisterDelegate); + + Assert.assertNull(assertSuccess(reregisterDelegate, Void.class)); + + // Unregistering should succeed. + final RequestDelegate unregisterDelegate = mock(RequestDelegate.class); + client.unregisterUserAgent(registerResponse.uaid, registerResponse.secret, unregisterDelegate); + + Assert.assertNull(assertSuccess(unregisterDelegate, Void.class)); + + // Trying to unregister a second time should give a 404. + final RequestDelegate reunregisterDelegate = mock(RequestDelegate.class); + client.unregisterUserAgent(registerResponse.uaid, registerResponse.secret, reunregisterDelegate); + + final AutopushClientException failureException = assertFailure(reunregisterDelegate, Void.class); + Assert.assertThat(failureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class)); + Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) failureException).isNotFound()); + } + + @Test + public void testChannel() throws Exception { + final RequestDelegate registerDelegate = mock(RequestDelegate.class); + client.registerUserAgent(Utils.generateGuid(), registerDelegate); + + final RegisterUserAgentResponse registerResponse = assertSuccess(registerDelegate, RegisterUserAgentResponse.class); + Assert.assertNotNull(registerResponse); + Assert.assertNotNull(registerResponse.uaid); + Assert.assertNotNull(registerResponse.secret); + + // We should be able to subscribe to a channel. + final RequestDelegate subscribeDelegate = mock(RequestDelegate.class); + client.subscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeDelegate); + + final SubscribeChannelResponse subscribeResponse = assertSuccess(subscribeDelegate, SubscribeChannelResponse.class); + Assert.assertNotNull(subscribeResponse); + Assert.assertNotNull(subscribeResponse.channelID); + Assert.assertNotNull(subscribeResponse.endpoint); + Assert.assertThat(subscribeResponse.endpoint, startsWith(FxAccountUtils.getAudienceForURL(serverURL))); + + // And we should be able to unsubscribe. + final RequestDelegate unsubscribeDelegate = mock(RequestDelegate.class); + client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeResponse.channelID, unsubscribeDelegate); + + Assert.assertNull(assertSuccess(unsubscribeDelegate, Void.class)); + + // Trying to unsubscribe a second time should give a 404. + final RequestDelegate reunsubscribeDelegate = mock(RequestDelegate.class); + client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeResponse.channelID, reunsubscribeDelegate); + + final AutopushClientException reunsubscribeFailureException = assertFailure(reunsubscribeDelegate, Void.class); + Assert.assertThat(reunsubscribeFailureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class)); + Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) reunsubscribeFailureException).isNotFound()); + + // Trying to unsubscribe from a non-existent channel should give a 404. Right now it gives a 401! + final RequestDelegate badUnsubscribeDelegate = mock(RequestDelegate.class); + client.unsubscribeChannel(registerResponse.uaid + "BAD", registerResponse.secret, subscribeResponse.channelID, badUnsubscribeDelegate); + + final AutopushClientException badUnsubscribeFailureException = assertFailure(badUnsubscribeDelegate, Void.class); + Assert.assertThat(badUnsubscribeFailureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class)); + Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) badUnsubscribeFailureException).isInvalidAuthentication()); + } +} diff --git a/testing/xpcshell/head.js b/testing/xpcshell/head.js index da78df981e28..414f376598b0 100644 --- a/testing/xpcshell/head.js +++ b/testing/xpcshell/head.js @@ -22,6 +22,7 @@ var _profileInitialized = false; _register_modules_protocol_handler(); var _Promise = Components.utils.import("resource://gre/modules/Promise.jsm", {}).Promise; +var _PromiseTestUtils = Components.utils.import("resource://testing-common/PromiseTestUtils.jsm", {}).PromiseTestUtils; // Support a common assertion library, Assert.jsm. var AssertCls = Components.utils.import("resource://testing-common/Assert.jsm", null).Assert; @@ -213,7 +214,6 @@ function _do_main() { function _do_quit() { _testLogger.info("exiting test"); - _Promise.Debugging.flushUncaughtErrors(); _quit = true; } @@ -499,16 +499,8 @@ function _execute_test() { // Call do_get_idle() to restore the factory and get the service. _fakeIdleService.activate(); - _Promise.Debugging.clearUncaughtErrorObservers(); - _Promise.Debugging.addUncaughtErrorObserver(function observer({message, date, fileName, stack, lineNumber}) { - let text = " A promise chain failed to handle a rejection: " + - message + " - rejection date: " + date; - _testLogger.error(text, - { - stack: _format_stack(stack), - source_file: fileName - }); - }); + _PromiseTestUtils.init(); + _PromiseTestUtils.Assert = Assert; // _HEAD_FILES is dynamically defined by . _load_files(_HEAD_FILES); @@ -539,6 +531,7 @@ function _execute_test() { } do_test_finished("MAIN run_test"); _do_main(); + _PromiseTestUtils.assertNoUncaughtRejections(); } catch (e) { _passed = false; // do_check failures are already logged and set _quit to true and throw @@ -613,8 +606,26 @@ function _execute_test() { // Restore idle service to avoid leaks. _fakeIdleService.deactivate(); - if (!_passed) - return; + if (_profileInitialized) { + // Since we have a profile, we will notify profile shutdown topics at + // the end of the current test, to ensure correct cleanup on shutdown. + let obs = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + obs.notifyObservers(null, "profile-change-net-teardown", null); + obs.notifyObservers(null, "profile-change-teardown", null); + obs.notifyObservers(null, "profile-before-change", null); + + _profileInitialized = false; + } + + try { + _PromiseTestUtils.ensureDOMPromiseRejectionsProcessed(); + _PromiseTestUtils.assertNoUncaughtRejections(); + _PromiseTestUtils.assertNoMoreExpectedRejections(); + } finally { + // It's important to terminate the module to avoid crashes on shutdown. + _PromiseTestUtils.uninit(); + } } /** @@ -1145,18 +1156,6 @@ function do_get_profile(notifyProfileAfterChange = false) { return null; } - if (!_profileInitialized) { - // Since we have a profile, we will notify profile shutdown topics at - // the end of the current test, to ensure correct cleanup on shutdown. - do_register_cleanup(function() { - let obsSvc = Components.classes["@mozilla.org/observer-service;1"]. - getService(Components.interfaces.nsIObserverService); - obsSvc.notifyObservers(null, "profile-change-net-teardown", null); - obsSvc.notifyObservers(null, "profile-change-teardown", null); - obsSvc.notifyObservers(null, "profile-before-change", null); - }); - } - let env = Components.classes["@mozilla.org/process/environment;1"] .getService(Components.interfaces.nsIEnvironment); // the python harness sets this in the environment for us @@ -1516,8 +1515,8 @@ function run_next_test() function _run_next_test() { if (_gTestIndex < _gTests.length) { - // Flush uncaught errors as early and often as possible. - _Promise.Debugging.flushUncaughtErrors(); + // Check for uncaught rejections as early and often as possible. + _PromiseTestUtils.assertNoUncaughtRejections(); let _properties; [_properties, _gRunningTest,] = _gTests[_gTestIndex++]; if (typeof(_properties.skip_if) == "function" && _properties.skip_if()) { @@ -1538,10 +1537,18 @@ function run_next_test() if (_properties._isTask) { _gTaskRunning = true; - _Task.spawn(_gRunningTest).then( - () => { _gTaskRunning = false; run_next_test(); }, - (ex) => { _gTaskRunning = false; do_report_unexpected_exception(ex); } - ); + _Task.spawn(_gRunningTest).then(() => { + _gTaskRunning = false; + run_next_test(); + }, ex => { + _gTaskRunning = false; + try { + do_report_unexpected_exception(ex); + } catch (ex) { + // The above throws NS_ERROR_ABORT and we don't want this to show up + // as an unhandled rejection later. + } + }); } else { // Exceptions do not kill asynchronous tests, so they'll time out. try { diff --git a/testing/xpcshell/selftest.py b/testing/xpcshell/selftest.py index 854d32689759..e389e4b4713c 100644 --- a/testing/xpcshell/selftest.py +++ b/testing/xpcshell/selftest.py @@ -35,6 +35,23 @@ TEST_FAIL_STRING = "TEST-UNEXPECTED-FAIL" SIMPLE_PASSING_TEST = "function run_test() { do_check_true(true); }" SIMPLE_FAILING_TEST = "function run_test() { do_check_true(false); }" +SIMPLE_UNCAUGHT_REJECTION_TEST = ''' +function run_test() { + Promise.reject(new Error("Test rejection.")); + do_check_true(true); +} +''' + +SIMPLE_UNCAUGHT_REJECTION_JSM_TEST = ''' +Components.utils.import("resource://gre/modules/Promise.jsm"); + +Promise.reject(new Error("Test rejection.")); + +function run_test() { + do_check_true(true); +} +''' + ADD_TEST_SIMPLE = ''' function run_test() { run_next_test(); } @@ -53,6 +70,26 @@ add_test(function test_failing() { }); ''' +ADD_TEST_UNCAUGHT_REJECTION = ''' +function run_test() { run_next_test(); } + +add_test(function test_uncaught_rejection() { + Promise.reject(new Error("Test rejection.")); + run_next_test(); +}); +''' + +ADD_TEST_UNCAUGHT_REJECTION_JSM = ''' +Components.utils.import("resource://gre/modules/Promise.jsm"); + +function run_test() { run_next_test(); } + +add_test(function test_uncaught_rejection() { + Promise.reject(new Error("Test rejection.")); + run_next_test(); +}); +''' + CHILD_TEST_PASSING = ''' function run_test () { run_next_test(); } @@ -424,6 +461,7 @@ tail = shuffle=shuffle, verbose=verbose, sequential=True, + testingModulesDir=os.path.join(objdir, '_tests', 'modules'), utility_path=self.utility_path), msg="""Tests should have %s, log: ======== @@ -802,6 +840,30 @@ add_test({ self.assertInLog(TEST_FAIL_STRING) self.assertNotInLog(TEST_PASS_STRING) + def testUncaughtRejection(self): + """ + Ensure a simple test with an uncaught rejection is reported. + """ + self.writeFile("test_simple_uncaught_rejection.js", SIMPLE_UNCAUGHT_REJECTION_TEST) + self.writeManifest(["test_simple_uncaught_rejection.js"]) + + self.assertTestResult(False) + self.assertEquals(1, self.x.testCount) + self.assertEquals(0, self.x.passCount) + self.assertEquals(1, self.x.failCount) + + def testUncaughtRejectionJSM(self): + """ + Ensure a simple test with an uncaught rejection from Promise.jsm is reported. + """ + self.writeFile("test_simple_uncaught_rejection_jsm.js", SIMPLE_UNCAUGHT_REJECTION_JSM_TEST) + self.writeManifest(["test_simple_uncaught_rejection_jsm.js"]) + + self.assertTestResult(False) + self.assertEquals(1, self.x.testCount) + self.assertEquals(0, self.x.passCount) + self.assertEquals(1, self.x.failCount) + def testAddTestSimple(self): """ Ensure simple add_test() works. @@ -839,6 +901,30 @@ add_test({ self.assertEquals(0, self.x.passCount) self.assertEquals(1, self.x.failCount) + def testAddTestUncaughtRejection(self): + """ + Ensure add_test() with an uncaught rejection is reported. + """ + self.writeFile("test_add_test_uncaught_rejection.js", ADD_TEST_UNCAUGHT_REJECTION) + self.writeManifest(["test_add_test_uncaught_rejection.js"]) + + self.assertTestResult(False) + self.assertEquals(1, self.x.testCount) + self.assertEquals(0, self.x.passCount) + self.assertEquals(1, self.x.failCount) + + def testAddTestUncaughtRejectionJSM(self): + """ + Ensure add_test() with an uncaught rejection from Promise.jsm is reported. + """ + self.writeFile("test_add_test_uncaught_rejection_jsm.js", ADD_TEST_UNCAUGHT_REJECTION_JSM) + self.writeManifest(["test_add_test_uncaught_rejection_jsm.js"]) + + self.assertTestResult(False) + self.assertEquals(1, self.x.testCount) + self.assertEquals(0, self.x.passCount) + self.assertEquals(1, self.x.failCount) + def testAddTaskTestSingle(self): """ Ensure add_test_task() with a single passing test works. diff --git a/toolkit/components/jsdownloads/src/DownloadCore.jsm b/toolkit/components/jsdownloads/src/DownloadCore.jsm index 01d5eabe6163..7b2a508165aa 100644 --- a/toolkit/components/jsdownloads/src/DownloadCore.jsm +++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm @@ -2101,6 +2101,9 @@ this.DownloadCopySaver.prototype = { // In case an error occurs while setting up the chain of objects for // the download, ensure that we release the resources of the saver. backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE); + // Since we're not going to handle deferSaveComplete.promise below, + // we need to make sure that the rejection is handled. + deferSaveComplete.promise.catch(() => {}); throw ex; } diff --git a/toolkit/components/jsdownloads/src/DownloadImport.jsm b/toolkit/components/jsdownloads/src/DownloadImport.jsm index ba85761c0ea2..6c175c0239cf 100644 --- a/toolkit/components/jsdownloads/src/DownloadImport.jsm +++ b/toolkit/components/jsdownloads/src/DownloadImport.jsm @@ -174,7 +174,7 @@ this.DownloadImport.prototype = { yield this.list.add(download); if (resumeDownload) { - download.start(); + download.start().catch(() => {}); } else { yield download.refresh(); } diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm index aeceb5f2b763..839035b9d448 100644 --- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm +++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm @@ -1080,7 +1080,7 @@ this.DownloadObserver = { this._wakeTimer = null; for (let download of this._canceledOfflineDownloads) { - download.start(); + download.start().catch(() => {}); } }, diff --git a/toolkit/components/jsdownloads/src/DownloadLegacy.js b/toolkit/components/jsdownloads/src/DownloadLegacy.js index 48218d87ad9a..c4b63808551a 100644 --- a/toolkit/components/jsdownloads/src/DownloadLegacy.js +++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js @@ -243,7 +243,7 @@ DownloadLegacyTransfer.prototype = { } // Start the download before allowing it to be controlled. Ignore errors. - aDownload.start().then(null, () => {}); + aDownload.start().catch(() => {}); // Start processing all the other events received through nsITransfer. this._deferDownload.resolve(aDownload); diff --git a/toolkit/components/jsdownloads/src/DownloadStore.jsm b/toolkit/components/jsdownloads/src/DownloadStore.jsm index 8ad7e720d633..83bc2824fbb0 100644 --- a/toolkit/components/jsdownloads/src/DownloadStore.jsm +++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm @@ -124,8 +124,8 @@ this.DownloadStore.prototype = { try { if (!download.succeeded && !download.canceled && !download.error) { // Try to restart the download if it was in progress during the - // previous session. - download.start(); + // previous session. Ignore errors. + download.start().catch(() => {}); } else { // If the download was not in progress, try to update the current // progress from disk. This is relevant in case we retained diff --git a/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js b/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js index 63e1174656b3..e08e500f78c8 100644 --- a/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js +++ b/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js @@ -88,7 +88,7 @@ add_task(function* test_cancel_pdf_download() { }); yield test_download_windowRef(tab, download); - download.start(); + download.start().catch(() => {}); // Immediately cancel the download to test that it is erased correctly. yield download.cancel(); diff --git a/toolkit/components/jsdownloads/test/unit/common_test_Download.js b/toolkit/components/jsdownloads/test/unit/common_test_Download.js index e353452f7d6e..8bedc32a627c 100644 --- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js +++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js @@ -32,7 +32,7 @@ function promiseStartDownload(aSourceUrl) { } return promiseNewDownload(aSourceUrl).then(download => { - download.start(); + download.start().catch(() => {}); return download; }); } @@ -64,7 +64,7 @@ function promiseStartDownload_tryToKeepPartialData() { partFilePath: targetFilePath + ".part" }, }); download.tryToKeepPartialData = true; - download.start(); + download.start().catch(() => {}); } else { // Start a download using nsIExternalHelperAppService, that is configured // to keep partially downloaded data by default. @@ -435,7 +435,7 @@ add_task(function* test_empty_progress_tryToKeepPartialData() partFilePath: targetFilePath + ".part" }, }); download.tryToKeepPartialData = true; - download.start(); + download.start().catch(() => {}); } else { // Start a download using nsIExternalHelperAppService, that is configured // to keep partially downloaded data by default. @@ -491,7 +491,7 @@ add_task(function* test_empty_noprogress() } }; - download.start(); + download.start().catch(() => {}); } else { // When testing DownloadLegacySaver, the download is already started when it // is created, and it may have already made all needed property change @@ -856,7 +856,7 @@ add_task(function* test_cancel_midway_restart_tryToKeepPartialData_false() // Restart the download from the beginning. mustInterruptResponses(); - download.start(); + download.start().catch(() => {}); yield promiseDownloadMidway(download); yield promisePartFileReady(download); @@ -1143,7 +1143,7 @@ add_task(function* test_whenSucceeded_after_restart() // we can verify getting a reference before the first download attempt. download = yield promiseNewDownload(httpUrl("interruptible.txt")); promiseSucceeded = download.whenSucceeded(); - download.start(); + download.start().catch(() => {}); } else { // When testing DownloadLegacySaver, the download is already started when it // is created, thus we cannot get the reference before the first attempt. @@ -1156,7 +1156,7 @@ add_task(function* test_whenSucceeded_after_restart() // The second request is allowed to complete. continueResponses(); - download.start(); + download.start().catch(() => {}); // Wait for the download to finish by waiting on the whenSucceeded promise. yield promiseSucceeded; @@ -1343,7 +1343,7 @@ add_task(function* test_error_restart() source: httpUrl("source.txt"), target: targetFile, }); - download.start(); + download.start().catch(() => {}); } else { download = yield promiseStartLegacyDownload(null, { targetFile: targetFile }); @@ -2186,7 +2186,7 @@ add_task(function* test_platform_integration() source: httpUrl("source.txt"), target: targetFile, }); - download.start(); + download.start().catch(() => {}); } // Wait for the whenSucceeded promise to be resolved first. diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js index 316407a81ee4..9839cd9da9f5 100644 --- a/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js @@ -215,7 +215,7 @@ add_task(function* test_notifications() let download3 = yield promiseNewDownload(httpUrl("interruptible.txt")); let promiseAttempt1 = download1.start(); let promiseAttempt2 = download2.start(); - download3.start(); + download3.start().catch(() => {}); // Add downloads to list. yield list.add(download1); @@ -250,8 +250,8 @@ add_task(function* test_no_notifications() let list = yield promiseNewList(isPrivate); let download1 = yield promiseNewDownload(httpUrl("interruptible.txt")); let download2 = yield promiseNewDownload(httpUrl("interruptible.txt")); - download1.start(); - download2.start(); + download1.start().catch(() => {}); + download2.start().catch(() => {}); // Add downloads to list. yield list.add(download1); @@ -316,7 +316,7 @@ add_task(function* test_suspend_resume() { return Task.spawn(function* () { let download = yield promiseNewDownload(httpUrl("interruptible.txt")); - download.start(); + download.start().catch(() => {}); list.add(download); return download; }); diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js index dfb97b6f45b7..936501691246 100644 --- a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js @@ -348,7 +348,7 @@ add_task(function* test_history_expiration() // Work with one finished download and one canceled download. yield downloadOne.start(); - downloadTwo.start(); + downloadTwo.start().catch(() => {}); yield downloadTwo.cancel(); // We must replace the visits added while executing the downloads with visits @@ -471,7 +471,7 @@ add_task(function* test_DownloadSummary() // Add a public download that has been canceled midway. let canceledPublicDownload = yield promiseNewDownload(httpUrl("interruptible.txt")); - canceledPublicDownload.start(); + canceledPublicDownload.start().catch(() => {}); yield promiseDownloadMidway(canceledPublicDownload); yield canceledPublicDownload.cancel(); yield publicList.add(canceledPublicDownload); @@ -479,7 +479,7 @@ add_task(function* test_DownloadSummary() // Add a public download that is in progress. let inProgressPublicDownload = yield promiseNewDownload(httpUrl("interruptible.txt")); - inProgressPublicDownload.start(); + inProgressPublicDownload.start().catch(() => {}); yield promiseDownloadMidway(inProgressPublicDownload); yield publicList.add(inProgressPublicDownload); @@ -488,7 +488,7 @@ add_task(function* test_DownloadSummary() source: { url: httpUrl("interruptible.txt"), isPrivate: true }, target: getTempFile(TEST_TARGET_FILE_NAME).path, }); - inProgressPrivateDownload.start(); + inProgressPrivateDownload.start().catch(() => {}); yield promiseDownloadMidway(inProgressPrivateDownload); yield privateList.add(inProgressPrivateDownload); diff --git a/toolkit/components/perfmonitoring/PerformanceStats.jsm b/toolkit/components/perfmonitoring/PerformanceStats.jsm index 6b1f32b12299..bef641e8fd07 100644 --- a/toolkit/components/perfmonitoring/PerformanceStats.jsm +++ b/toolkit/components/perfmonitoring/PerformanceStats.jsm @@ -95,7 +95,15 @@ Probe.prototype = { release: function() { this._counter--; if (this._counter == 0) { - this._impl.isActive = false; + try { + this._impl.isActive = false; + } catch (ex) { + if (ex && typeof ex == "object" && ex.result == Components.results.NS_ERROR_NOT_AVAILABLE) { + // The service has already been shutdown. Ignore further shutdown requests. + return; + } + throw ex; + } Process.broadcast("release", [this._name]); } }, diff --git a/toolkit/components/search/nsSearchService.js b/toolkit/components/search/nsSearchService.js index 0b50401b4db5..3e10ac69e596 100644 --- a/toolkit/components/search/nsSearchService.js +++ b/toolkit/components/search/nsSearchService.js @@ -4453,6 +4453,13 @@ SearchService.prototype = { }, _addObservers: function SRCH_SVC_addObservers() { + if (this._observersAdded) { + // There might be a race between synchronous and asynchronous + // initialization for which we try to register the observers twice. + return; + } + this._observersAdded = true; + Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, false); Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC, false); @@ -4495,6 +4502,7 @@ SearchService.prototype = { () => shutdownState ); }, + _observersAdded: false, _removeObservers: function SRCH_SVC_removeObservers() { Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC); diff --git a/toolkit/components/search/tests/xpcshell/test_hidden.js b/toolkit/components/search/tests/xpcshell/test_hidden.js index f49344cd8f76..0079d05a1a84 100644 --- a/toolkit/components/search/tests/xpcshell/test_hidden.js +++ b/toolkit/components/search/tests/xpcshell/test_hidden.js @@ -47,10 +47,6 @@ add_task(function* async_init() { add_task(function* sync_init() { let reInitPromise = asyncReInit(); // Synchronously check the current default engine, to force a sync init. - // XXX For some reason forcing a sync init while already asynchronously - // reinitializing causes a shutdown warning related to engineMetadataService's - // finalize method having already been called. Seems harmless for the purpose - // of this test. do_check_false(Services.search.isInitialized); do_check_eq(Services.search.currentEngine.name, "hidden"); do_check_true(Services.search.isInitialized); diff --git a/toolkit/components/telemetry/docs/heartbeat-ping.rst b/toolkit/components/telemetry/docs/heartbeat-ping.rst new file mode 100644 index 000000000000..1b4fc1e588ad --- /dev/null +++ b/toolkit/components/telemetry/docs/heartbeat-ping.rst @@ -0,0 +1,61 @@ + +"heartbeat" ping +================= + +This ping is submitted after a Firefox Heartbeat survey. Even if the user exits +the browser, closes the survey window, or ignores the survey, Heartbeat will +provide a ping to Telemetry for sending during the same session. + +The payload contains the user's survey response (if any) as well as timestamps +of various Heartbeat events (survey shown, survey closed, link clicked, etc). + +The ping will also report the "surveyId", "surveyVersion" and "testing" +Heartbeat survey parameters (if they are present in the survey config). +These "meta fields" will be repeated verbatim in the payload section. + +The environment block and client ID are submitted with this ping. + +Structure:: + + { + type: "heartbeat", + version: 4, + clientId: , + environment: { ... } + ... common ping data ... + payload: { + version: 1, + flowId: , + ... timestamps below ... + offeredTS: , + learnMoreTS: , + votedTS: , + engagedTS: , + closedTS: , + expiredTS: , + windowClosedTS: , + ... user's rating below ... + score: , + ... survey meta fields below ... + surveyId: , + surveyVersion: , + testing: + } + } + +Notes: + +* Pings will **NOT** have all possible timestamps, timestamps are only reported for events that actually occurred. +* Timestamp meanings: + * offeredTS: when the survey was shown to the user + * learnMoreTS: when the user clicked on the "Learn More" link + * votedTS: when the user voted + * engagedTS: when the user clicked on the survey-provided button (alternative to voting feature) + * closedTS: when the Heartbeat notification bar was closed + * expiredTS: indicates that the survey expired after 2 hours of no interaction (threshold regulated by "browser.uitour.surveyDuration" pref) + * windowClosedTS: the user closed the entire Firefox window containing the survey, thus ending the survey. This timestamp will also be reported when the survey is ended by the browser being shut down. +* The surveyId/surveyVersion fields identify a specific survey (like a "1040EZ" tax paper form). The flowID is a UUID that uniquely identifies a single user's interaction with the survey. Think of it as a session token. +* The self-support page cannot include additional data in this payload. Only the the 4 flowId/surveyId/surveyVersion/testing fields are under the self-support page's control. + +See also: :doc:`common ping fields ` + diff --git a/toolkit/components/telemetry/docs/index.rst b/toolkit/components/telemetry/docs/index.rst index be6eeb4b31a0..90c5e537b86c 100644 --- a/toolkit/components/telemetry/docs/index.rst +++ b/toolkit/components/telemetry/docs/index.rst @@ -23,5 +23,6 @@ Client-side, this consists of: deletion-ping crash-ping uitour-ping + heartbeat-ping preferences crashes diff --git a/toolkit/components/telemetry/docs/pings.rst b/toolkit/components/telemetry/docs/pings.rst index 5695fc394027..9933be02c08c 100644 --- a/toolkit/components/telemetry/docs/pings.rst +++ b/toolkit/components/telemetry/docs/pings.rst @@ -48,6 +48,7 @@ Ping types * :doc:`uitour-ping` - a ping submitted via the UITour API * ``activation`` - *planned* - sent right after installation or profile creation * ``upgrade`` - *planned* - sent right after an upgrade +* :doc:`heartbeat-ping` - contains information on Heartbeat surveys * :doc:`deletion ` - sent when FHR upload is disabled, requesting deletion of the data associated with this user Archiving diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index 7cd756839147..e187b6685a7c 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -9,6 +9,10 @@ BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini'] MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini'] MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini'] +TESTING_JS_MODULES += [ + 'tests/PromiseTestUtils.jsm', +] + SPHINX_TREES['toolkit_modules'] = 'docs' EXTRA_JS_MODULES += [ diff --git a/toolkit/modules/tests/PromiseTestUtils.jsm b/toolkit/modules/tests/PromiseTestUtils.jsm new file mode 100644 index 000000000000..d60b785a580e --- /dev/null +++ b/toolkit/modules/tests/PromiseTestUtils.jsm @@ -0,0 +1,241 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Detects and reports unhandled rejections during test runs. Test harnesses + * will fail tests in this case, unless the test whitelists itself. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "PromiseTestUtils", +]; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Services.jsm", this); + +// Keep "JSMPromise" separate so "Promise" still refers to DOM Promises. +let JSMPromise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; + +// For now, we need test harnesses to provide a reference to Assert.jsm. +let Assert = null; + +this.PromiseTestUtils = { + /** + * Array of objects containing the details of the Promise rejections that are + * currently left uncaught. This includes DOM Promise and Promise.jsm. When + * rejections in DOM Promises are consumed, they are removed from this list. + * + * The objects contain at least the following properties: + * { + * message: The error message associated with the rejection, if any. + * date: Date object indicating when the rejection was observed. + * id: For DOM Promise only, the Promise ID from PromiseDebugging. This is + * only used for tracking and should not be checked by the callers. + * stack: nsIStackFrame, SavedFrame, or string indicating the stack at the + * time the rejection was triggered. May also be null if the + * rejection was triggered while a script was on the stack. + * } + */ + _rejections: [], + + /** + * When an uncaught rejection is detected, it is ignored if one of the + * functions in this array returns true when called with the rejection details + * as its only argument. When a function matches an expected rejection, it is + * then removed from the array. + */ + _rejectionIgnoreFns: [], + + /** + * Called only by the test infrastructure, registers the rejection observers. + * + * This should be called only once, and a matching "uninit" call must be made + * or the tests will crash on shutdown. + */ + init() { + if (this._initialized) { + Cu.reportError("This object was already initialized."); + return; + } + + PromiseDebugging.addUncaughtRejectionObserver(this); + + // Promise.jsm rejections are only reported to this observer when requested, + // so we don't have to store a key to remove them when consumed. + JSMPromise.Debugging.addUncaughtErrorObserver( + rejection => this._rejections.push(rejection)); + + this._initialized = true; + }, + _initialized: false, + + /** + * Called only by the test infrastructure, unregisters the observers. + */ + uninit() { + if (!this._initialized) { + return; + } + + PromiseDebugging.removeUncaughtRejectionObserver(this); + JSMPromise.Debugging.clearUncaughtErrorObservers(); + + this._initialized = false; + }, + + /** + * Called only by the test infrastructure, spins the event loop until the + * messages for pending DOM Promise rejections have been processed. + */ + ensureDOMPromiseRejectionsProcessed() { + let observed = false; + let observer = { + onLeftUncaught: promise => { + if (PromiseDebugging.getState(promise).reason === + this._ensureDOMPromiseRejectionsProcessedReason) { + observed = true; + } + }, + onConsumed() {}, + }; + + PromiseDebugging.addUncaughtRejectionObserver(observer); + Promise.reject(this._ensureDOMPromiseRejectionsProcessedReason); + while (!observed) { + Services.tm.mainThread.processNextEvent(true); + } + PromiseDebugging.removeUncaughtRejectionObserver(observer); + }, + _ensureDOMPromiseRejectionsProcessedReason: {}, + + /** + * Called only by the tests for PromiseDebugging.addUncaughtRejectionObserver + * and for JSMPromise.Debugging, disables the observers in this module. + */ + disableUncaughtRejectionObserverForSelfTest() { + this.uninit(); + }, + + /** + * Called by tests that have been whitelisted, disables the observers in this + * module. For new tests where uncaught rejections are expected, you should + * use the more granular expectUncaughtRejection function instead. + */ + thisTestLeaksUncaughtRejectionsAndShouldBeFixed() { + this.uninit(); + }, + + /** + * Sets or updates the Assert object instance to be used for error reporting. + */ + set Assert(assert) { + Assert = assert; + }, + + // UncaughtRejectionObserver + onLeftUncaught(promise) { + let message = "(Unable to convert rejection reason to string.)"; + try { + let reason = PromiseDebugging.getState(promise).reason; + if (reason === this._ensureDOMPromiseRejectionsProcessedReason) { + // Ignore the special promise for ensureDOMPromiseRejectionsProcessed. + return; + } + message = reason.message || ("" + reason); + } catch (ex) {} + + // It's important that we don't store any reference to the provided Promise + // object or its value after this function returns in order to avoid leaks. + this._rejections.push({ + id: PromiseDebugging.getPromiseID(promise), + message, + date: new Date(), + stack: PromiseDebugging.getRejectionStack(promise), + }); + }, + + // UncaughtRejectionObserver + onConsumed(promise) { + // We don't expect that many unhandled rejections will appear at the same + // time, so the algorithm doesn't need to be optimized for that case. + let id = PromiseDebugging.getPromiseID(promise); + let index = this._rejections.findIndex(rejection => rejection.id == id); + // If we get a consumption notification for a rejection that was left + // uncaught before this module was initialized, we can safely ignore it. + if (index != -1) { + this._rejections.splice(index, 1); + } + }, + + /** + * Informs the test suite that the test code will generate a Promise rejection + * that will still be unhandled when the test file terminates. + * + * This method must be called once for each instance of Promise that is + * expected to be uncaught, even if the rejection reason is the same for each + * instance. + * + * If the expected rejection does not occur, the test will fail. + * + * @param regExpOrCheckFn + * This can either be a regular expression that should match the error + * message of the rejection, or a check function that is invoked with + * the rejection details object as its first argument. + */ + expectUncaughtRejection(regExpOrCheckFn) { + let checkFn = !("test" in regExpOrCheckFn) ? regExpOrCheckFn : + rejection => regExpOrCheckFn.test(rejection.message); + this._rejectionIgnoreFns.push(checkFn); + }, + + /** + * Fails the test if there are any uncaught rejections at this time that have + * not been whitelisted using expectUncaughtRejection. + * + * Depending on the configuration of the test suite, this function might only + * report the details of the first uncaught rejection that was generated. + * + * This is called by the test suite at the end of each test function. + */ + assertNoUncaughtRejections() { + // Ask Promise.jsm to report all uncaught rejections to the observer now. + JSMPromise.Debugging.flushUncaughtErrors(); + + // If there is any uncaught rejection left at this point, the test fails. + while (this._rejections.length > 0) { + let rejection = this._rejections.shift(); + + // If one of the ignore functions matches, ignore the rejection, then + // remove the function so that each function only matches one rejection. + let index = this._rejectionIgnoreFns.findIndex(f => f(rejection)); + if (index != -1) { + this._rejectionIgnoreFns.splice(index, 1); + continue; + } + + // Report the error. This operation can throw an exception, depending on + // the configuration of the test suite that handles the assertion. + Assert.ok(false, + `A promise chain failed to handle a rejection:` + + ` ${rejection.message} - rejection date: ${rejection.date}`+ + ` - stack: ${rejection.stack}`); + } + }, + + /** + * Fails the test if any rejection indicated by expectUncaughtRejection has + * not yet been reported at this time. + * + * This is called by the test suite at the end of each test file. + */ + assertNoMoreExpectedRejections() { + // Only log this condition is there is a failure. + if (this._rejectionIgnoreFns.length > 0) { + Assert.equal(this._rejectionIgnoreFns.length, 0, + "Unable to find a rejection expected by expectUncaughtRejection."); + } + }, +}; diff --git a/toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js b/toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js index b3a8863cda9b..44572e6001a3 100644 --- a/toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js +++ b/toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js @@ -1,12 +1,7 @@ "use strict"; var {ObjectUtils} = Components.utils.import("resource://gre/modules/ObjectUtils.jsm", {}); -var {Promise} = Components.utils.import("resource://gre/modules/Promise.jsm", {}); - -add_task(function* init() { - // The code will cause uncaught rejections on purpose. - Promise.Debugging.clearUncaughtErrorObservers(); -}); +var {PromiseTestUtils} = Components.utils.import("resource://testing-common/PromiseTestUtils.jsm", {}); add_task(function* test_strict() { let loose = { a: 1 }; @@ -16,11 +11,13 @@ add_task(function* test_strict() { loose.b || undefined; // Should not throw. strict.a; // Should not throw. + PromiseTestUtils.expectUncaughtRejection(/No such property: "b"/); Assert.throws(() => strict.b, /No such property: "b"/); "b" in strict; // Should not throw. strict.b = 2; strict.b; // Should not throw. + PromiseTestUtils.expectUncaughtRejection(/No such property: "c"/); Assert.throws(() => strict.c, /No such property: "c"/); "c" in strict; // Should not throw. loose.c = 3; diff --git a/toolkit/modules/tests/xpcshell/test_Promise.js b/toolkit/modules/tests/xpcshell/test_Promise.js index ab2f3b233845..6f39bb0fd2e1 100644 --- a/toolkit/modules/tests/xpcshell/test_Promise.js +++ b/toolkit/modules/tests/xpcshell/test_Promise.js @@ -5,10 +5,10 @@ Components.utils.import("resource://gre/modules/Promise.jsm"); Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/Task.jsm"); +Components.utils.import("resource://testing-common/PromiseTestUtils.jsm"); -// Deactivate the standard xpcshell observer, as it turns uncaught -// rejections into failures, which we don't want here. -Promise.Debugging.clearUncaughtErrorObservers(); +// Prevent test failures due to the unhandled rejections in this test file. +PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest(); //////////////////////////////////////////////////////////////////////////////// //// Test runner diff --git a/toolkit/modules/tests/xpcshell/test_PromiseUtils.js b/toolkit/modules/tests/xpcshell/test_PromiseUtils.js index ff9ba3688b51..5f1ac8b7d0b7 100644 --- a/toolkit/modules/tests/xpcshell/test_PromiseUtils.js +++ b/toolkit/modules/tests/xpcshell/test_PromiseUtils.js @@ -5,6 +5,7 @@ Components.utils.import("resource://gre/modules/PromiseUtils.jsm"); Components.utils.import("resource://gre/modules/Timer.jsm"); +Components.utils.import("resource://testing-common/PromiseTestUtils.jsm"); // Tests for PromiseUtils.jsm function run_test() { @@ -98,8 +99,9 @@ add_task(function* test_reject_resolved_promise() { /* Test for the case when a rejected Promise is * passed to the reject method */ add_task(function* test_reject_resolved_promise() { + PromiseTestUtils.expectUncaughtRejection(/This one rejects/); let def = PromiseUtils.defer(); - let p = new Promise((resolve, reject) => reject(new Error("This on rejects"))); + let p = new Promise((resolve, reject) => reject(new Error("This one rejects"))); def.reject(p); yield Assert.rejects(def.promise, Promise, "Rejection with a rejected promise uses the passed promise itself as the reason of rejection"); }); diff --git a/widget/windows/TSFTextStore.cpp b/widget/windows/TSFTextStore.cpp index e77478fbe215..6dee2057f009 100644 --- a/widget/windows/TSFTextStore.cpp +++ b/widget/windows/TSFTextStore.cpp @@ -3028,10 +3028,21 @@ TSFTextStore::InsertEmbedded(DWORD dwFlags, } void -TSFTextStore::SetInputScope(const nsString& aHTMLInputType) +TSFTextStore::SetInputScope(const nsString& aHTMLInputType, + const nsString& aHTMLInputInputMode) { mInputScopes.Clear(); if (aHTMLInputType.IsEmpty() || aHTMLInputType.EqualsLiteral("text")) { + if (aHTMLInputInputMode.EqualsLiteral("url")) { + mInputScopes.AppendElement(IS_URL); + } else if (aHTMLInputInputMode.EqualsLiteral("email")) { + mInputScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS); + } else if (aHTMLInputType.EqualsLiteral("tel")) { + mInputScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER); + mInputScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER); + } else if (aHTMLInputType.EqualsLiteral("numeric")) { + mInputScopes.AppendElement(IS_NUMBER); + } return; } @@ -4559,7 +4570,8 @@ TSFTextStore::CreateAndSetFocus(nsWindowBase* aFocusedWidget, "ITfTheadMgr::AssociateFocus() failure")); return false; } - sEnabledTextStore->SetInputScope(aContext.mHTMLInputType); + sEnabledTextStore->SetInputScope(aContext.mHTMLInputType, + aContext.mHTMLInputInputmode); if (sEnabledTextStore->mSink) { MOZ_LOG(sTextStoreLog, LogLevel::Info, @@ -5296,7 +5308,8 @@ TSFTextStore::SetInputContext(nsWindowBase* aWidget, if (aAction.mFocusChange != InputContextAction::FOCUS_NOT_CHANGED) { if (sEnabledTextStore) { - sEnabledTextStore->SetInputScope(aContext.mHTMLInputType); + sEnabledTextStore->SetInputScope(aContext.mHTMLInputType, + aContext.mHTMLInputInputmode); } return; } diff --git a/widget/windows/TSFTextStore.h b/widget/windows/TSFTextStore.h index 2eb994d66473..1b5451e4a5c6 100644 --- a/widget/windows/TSFTextStore.h +++ b/widget/windows/TSFTextStore.h @@ -302,7 +302,8 @@ protected: HRESULT HandleRequestAttrs(DWORD aFlags, ULONG aFilterCount, const TS_ATTRID* aFilterAttrs); - void SetInputScope(const nsString& aHTMLInputType); + void SetInputScope(const nsString& aHTMLInputType, + const nsString& aHTMLInputInputmode); // Creates native caret over our caret. This method only works on desktop // application. Otherwise, this does nothing. diff --git a/widget/windows/WinIMEHandler.cpp b/widget/windows/WinIMEHandler.cpp index c45a14e60d48..261988cb24b6 100644 --- a/widget/windows/WinIMEHandler.cpp +++ b/widget/windows/WinIMEHandler.cpp @@ -393,7 +393,7 @@ IMEHandler::OnDestroyWindow(nsWindow* aWindow) if (!sIsInTSFMode) { // MSDN says we need to set IS_DEFAULT to avoid memory leak when we use // SetInputScopes API. Use an empty string to do this. - SetInputScopeForIMM32(aWindow, EmptyString()); + SetInputScopeForIMM32(aWindow, EmptyString(), EmptyString()); } #endif // #ifdef NS_ENABLE_TSF AssociateIMEContext(aWindow, true); @@ -444,7 +444,8 @@ IMEHandler::SetInputContext(nsWindow* aWindow, } } else { // Set at least InputScope even when TextStore is not available. - SetInputScopeForIMM32(aWindow, aInputContext.mHTMLInputType); + SetInputScopeForIMM32(aWindow, aInputContext.mHTMLInputType, + aInputContext.mHTMLInputInputmode); } #endif // #ifdef NS_ENABLE_TSF @@ -517,7 +518,8 @@ IMEHandler::CurrentKeyboardLayoutHasIME() // static void IMEHandler::SetInputScopeForIMM32(nsWindow* aWindow, - const nsAString& aHTMLInputType) + const nsAString& aHTMLInputType, + const nsAString& aHTMLInputInputmode) { if (sIsInTSFMode || !sSetInputScopes || aWindow->Destroyed()) { return; @@ -526,9 +528,28 @@ IMEHandler::SetInputScopeForIMM32(nsWindow* aWindow, const InputScope* scopes = nullptr; // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html if (aHTMLInputType.IsEmpty() || aHTMLInputType.EqualsLiteral("text")) { - static const InputScope inputScopes[] = { IS_DEFAULT }; - scopes = &inputScopes[0]; - arraySize = ArrayLength(inputScopes); + if (aHTMLInputInputmode.EqualsLiteral("url")) { + static const InputScope inputScopes[] = { IS_URL }; + scopes = &inputScopes[0]; + arraySize = ArrayLength(inputScopes); + } else if (aHTMLInputInputmode.EqualsLiteral("email")) { + static const InputScope inputScopes[] = { IS_EMAIL_SMTPEMAILADDRESS }; + scopes = &inputScopes[0]; + arraySize = ArrayLength(inputScopes); + } else if (aHTMLInputInputmode.EqualsLiteral("tel")) { + static const InputScope inputScopes[] = + {IS_TELEPHONE_LOCALNUMBER, IS_TELEPHONE_FULLTELEPHONENUMBER}; + scopes = &inputScopes[0]; + arraySize = ArrayLength(inputScopes); + } else if (aHTMLInputInputmode.EqualsLiteral("numeric")) { + static const InputScope inputScopes[] = { IS_NUMBER }; + scopes = &inputScopes[0]; + arraySize = ArrayLength(inputScopes); + } else { + static const InputScope inputScopes[] = { IS_DEFAULT }; + scopes = &inputScopes[0]; + arraySize = ArrayLength(inputScopes); + } } else if (aHTMLInputType.EqualsLiteral("url")) { static const InputScope inputScopes[] = { IS_URL }; scopes = &inputScopes[0]; diff --git a/widget/windows/WinIMEHandler.h b/widget/windows/WinIMEHandler.h index 790c21183337..9df8c87a8dd8 100644 --- a/widget/windows/WinIMEHandler.h +++ b/widget/windows/WinIMEHandler.h @@ -131,7 +131,8 @@ private: #ifdef NS_ENABLE_TSF static decltype(SetInputScopes)* sSetInputScopes; static void SetInputScopeForIMM32(nsWindow* aWindow, - const nsAString& aHTMLInputType); + const nsAString& aHTMLInputType, + const nsAString& aHTMLInputInputmode); static bool sIsInTSFMode; // If sIMMEnabled is false, any IME messages are not handled in TSF mode. // Additionally, IME context is always disassociated from focused window.