mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 14:22:01 +00:00
Merge mozilla-central to mozilla-inbound
This commit is contained in:
commit
43ec756ee5
@ -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
|
||||
|
@ -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");
|
||||
|
@ -812,30 +812,6 @@
|
||||
command="Browser:ShowAllBookmarks"
|
||||
key="manBookmarkKb"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="BMB_subscribeToPageMenuitem"
|
||||
#ifndef XP_MACOSX
|
||||
class="menuitem-iconic subviewbutton"
|
||||
#else
|
||||
class="subviewbutton"
|
||||
#endif
|
||||
label="&subscribeToPageMenuitem.label;"
|
||||
oncommand="return FeedHandler.subscribeToFeed(null, event);"
|
||||
onclick="checkForMiddleClick(this, event);"
|
||||
observes="singleFeedMenuitemState"/>
|
||||
<menu id="BMB_subscribeToPageMenupopup"
|
||||
#ifndef XP_MACOSX
|
||||
class="menu-iconic subviewbutton"
|
||||
#else
|
||||
class="subviewbutton"
|
||||
#endif
|
||||
label="&subscribeToPageMenupopup.label;"
|
||||
observes="multipleFeedsMenuState">
|
||||
<menupopup id="BMB_subscribeToPageSubmenuMenupopup"
|
||||
onpopupshowing="return FeedHandler.buildFeedList(event.target);"
|
||||
oncommand="return FeedHandler.subscribeToFeed(null, event);"
|
||||
onclick="checkForMiddleClick(this, event);"/>
|
||||
</menu>
|
||||
<menuseparator/>
|
||||
<menu id="BMB_bookmarksToolbar"
|
||||
class="menu-iconic bookmark-item subviewbutton"
|
||||
label="&personalbarCmd.label;"
|
||||
|
@ -692,20 +692,6 @@ Sanitizer.onStartup = Task.async(function*() {
|
||||
shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown",
|
||||
() => 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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -37,7 +37,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
<html:input anonid="input"
|
||||
class="autocomplete-textbox urlbar-input textbox-input uri-element-right-align"
|
||||
allowevents="true"
|
||||
xbl:inherits="tooltiptext=inputtooltiptext,value,type=inputtype,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/>
|
||||
inputmode="url"
|
||||
xbl:inherits="tooltiptext=inputtooltiptext,value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/>
|
||||
</xul:hbox>
|
||||
<xul:dropmarker anonid="historydropmarker"
|
||||
class="autocomplete-history-dropmarker urlbar-history-dropmarker"
|
||||
|
@ -246,7 +246,7 @@
|
||||
<panelview id="PanelUI-loopapi" flex="1"/>
|
||||
|
||||
<panelview id="PanelUI-feeds" flex="1" oncommand="FeedHandler.subscribeToFeed(null, event);">
|
||||
<label value="&feedsMenu.label;" class="panel-subview-header"/>
|
||||
<label value="&feedsMenu2.label;" class="panel-subview-header"/>
|
||||
</panelview>
|
||||
|
||||
<panelview id="PanelUI-helpView" flex="1" class="PanelUI-subView">
|
||||
|
@ -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) {
|
||||
|
@ -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}");
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
},
|
||||
];
|
||||
|
@ -163,7 +163,7 @@ These should match what Safari and other Apple applications use on OS X Lion. --
|
||||
<!ENTITY shareSelect.accesskey "r">
|
||||
<!ENTITY shareVideo.label "Share This Video">
|
||||
<!ENTITY shareVideo.accesskey "r">
|
||||
<!ENTITY feedsMenu.label "Subscribe">
|
||||
<!ENTITY feedsMenu2.label "Subscribe to This Page">
|
||||
<!ENTITY subscribeToPageMenupopup.label "Subscribe to This Page">
|
||||
<!ENTITY subscribeToPageMenuitem.label "Subscribe to This Page…">
|
||||
<!ENTITY addCurPagesCmd.label "Bookmark All Tabs…">
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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
|
@ -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;
|
@ -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']
|
||||
|
5
devtools/client/performance/components/test/chrome.ini
Normal file
5
devtools/client/performance/components/test/chrome.ini
Normal file
@ -0,0 +1,5 @@
|
||||
[DEFAULT]
|
||||
support-files =
|
||||
head.js
|
||||
|
||||
[test_jit_optimizations_01.html]
|
181
devtools/client/performance/components/test/head.js
Normal file
181
devtools/client/performance/components/test/head.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,70 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the rendering of the JIT Optimizations tree. Tests when jit data has observed types, multiple observed types, multiple sites, a site with a successful strategy, site with no successful strategy.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>JITOptimizations component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body style="height: 10000px;">
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
|
||||
let React = browserRequire("devtools/client/shared/vendor/react");
|
||||
let JITOptimizations = React.createFactory(browserRequire("devtools/client/performance/components/jit-optimizations"));
|
||||
ok(JITOptimizations, "Should get JITOptimizations");
|
||||
let opts;
|
||||
|
||||
opts = ReactDOM.render(JITOptimizations({
|
||||
onViewSourceInDebugger: function(){},
|
||||
frameData: {
|
||||
isMetaCategory: false,
|
||||
url: "http://internet.com/file.js",
|
||||
line: 1,
|
||||
functionName: "myfunc",
|
||||
},
|
||||
optimizationSites: OPTS_DATA_GENERAL,
|
||||
autoExpandDepth: 1000,
|
||||
}), window.document.body);
|
||||
yield forceRender(opts);
|
||||
|
||||
checkOptimizationHeader("myfunc", "file.js", "1");
|
||||
|
||||
checkOptimizationTree([
|
||||
{ type: "site", strategy: "GetElem_TypedArray", samples: "90" },
|
||||
{ type: "types", count: "2" },
|
||||
{ type: "type", site: "Receiver", mirType: "Object" },
|
||||
{ type: "observedtype", keyedBy: "constructor", name: "MyView" },
|
||||
{ type: "type", site: "Index", mirType: "Int32" },
|
||||
{ type: "attempts", count: "4" },
|
||||
{ type: "attempt", strategy: "GetElem_TypedObject", outcome: "AccessNotTypedObject" },
|
||||
{ type: "attempt", strategy: "GetElem_Dense", outcome: "AccessNotDense" },
|
||||
{ type: "attempt", strategy: "GetElem_TypedStatic", outcome: "Disabled" },
|
||||
{ type: "attempt", strategy: "GetElem_TypedArray", outcome: "GenericSuccess", success: true },
|
||||
{ type: "site", strategy: "Call_Inline", samples: "100", failureIcon: true },
|
||||
{ type: "types", count: "1" },
|
||||
{ type: "type", site: "Call_Target", mirType: "Object" },
|
||||
{ type: "observedtype", keyedBy: "primitive" },
|
||||
{ type: "observedtype", keyedBy: "constructor", name: "B" },
|
||||
{ type: "observedtype", keyedBy: "constructor", name: "C" },
|
||||
{ type: "observedtype", keyedBy: "constructor", name: "D" },
|
||||
{ type: "attempts", count: "1" },
|
||||
{ type: "attempt", strategy: "Call_Inline", outcome: "CantInlineBigData" },
|
||||
]);
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -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<string>} 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<OptimizationAttempt>}
|
||||
*/
|
||||
|
||||
OptimizationSite.prototype.getAttempts = function () {
|
||||
return this.data.attempts;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all IonTypes in this OptimizationSite.
|
||||
*
|
||||
* @return {Array<IonType>}
|
||||
*/
|
||||
|
||||
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;
|
||||
|
@ -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.";
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -94,7 +94,7 @@ var JsCallTreeView = Heritage.extend(DetailsSubview, {
|
||||
? frameNode.getOptimizations().optimizationSites
|
||||
: [];
|
||||
|
||||
let optimizations = Optimizations({
|
||||
let optimizations = JITOptimizationsView({
|
||||
frameData,
|
||||
optimizationSites,
|
||||
onViewSourceInDebugger: (url, line) => {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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<lineEndPos; i++) {
|
||||
for (let i = inputNode.selectionEnd; i < lineEndPos; i++) {
|
||||
if (inputValue.charAt(i) == "\r" ||
|
||||
inputValue.charAt(i) == "\n") {
|
||||
lineEndPos = i;
|
||||
@ -3822,9 +3855,9 @@ JSTerm.prototype = {
|
||||
this.canCaretGoNext() &&
|
||||
this.historyPeruse(HISTORY_FORWARD)) {
|
||||
event.preventDefault();
|
||||
// Ctrl-N is also used to focus the Network category button on MacOSX.
|
||||
// The preventDefault() call doesn't prevent the focus from moving
|
||||
// away from the input.
|
||||
// Ctrl-N is also used to focus the Network category button on
|
||||
// MacOSX. The preventDefault() call doesn't prevent the focus
|
||||
// from moving away from the input.
|
||||
inputNode.focus();
|
||||
}
|
||||
this.clearCompletion();
|
||||
@ -3955,10 +3988,12 @@ JSTerm.prototype = {
|
||||
|
||||
case Ci.nsIDOMKeyEvent.DOM_VK_END:
|
||||
if (this.autocompletePopup.isOpen) {
|
||||
this.autocompletePopup.selectedIndex = this.autocompletePopup.itemCount - 1;
|
||||
this.autocompletePopup.selectedIndex =
|
||||
this.autocompletePopup.itemCount - 1;
|
||||
event.preventDefault();
|
||||
} else if (inputValue.length <= 0) {
|
||||
this.hud.outputWrapper.scrollTop = this.hud.outputWrapper.scrollHeight;
|
||||
this.hud.outputWrapper.scrollTop =
|
||||
this.hud.outputWrapper.scrollHeight;
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
@ -4037,14 +4072,14 @@ JSTerm.prototype = {
|
||||
// the user is already at the last entry.
|
||||
// Note: this code does not store changes to items that are already in
|
||||
// history.
|
||||
if (this.historyPlaceHolder+1 == this.historyIndex) {
|
||||
if (this.historyPlaceHolder + 1 == this.historyIndex) {
|
||||
this.history[this.historyIndex] = this.getInputValue() || "";
|
||||
}
|
||||
|
||||
this.setInputValue(inputVal);
|
||||
} else if (direction == HISTORY_FORWARD) {
|
||||
// Down Arrow key
|
||||
if (this.historyPlaceHolder >= (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.
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
};
|
||||
|
@ -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");
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.");
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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',
|
||||
|
@ -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<Void, Void, Void>() {
|
||||
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<String> 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;
|
||||
}
|
||||
|
||||
|
@ -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<Void, Void, Void>() {
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
public void run() {
|
||||
GeckoAppShell.createShortcut(displayTitle, info.url);
|
||||
return null;
|
||||
|
||||
}
|
||||
}.execute();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
* <p/>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<T> {
|
||||
void handleError(Exception e);
|
||||
void handleFailure(AutopushClientException e);
|
||||
void handleSuccess(T result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Intepret a response from the autopush server.
|
||||
* <p>
|
||||
* 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 <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate<T> 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.
|
||||
* <p>
|
||||
* Override <code>handleSuccess</code> to parse the body of the resource
|
||||
* request and call the request callback. <code>handleSuccess</code> is
|
||||
* invoked via the executor, so you don't need to delegate further.
|
||||
*/
|
||||
protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
|
||||
protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
|
||||
|
||||
protected final String secret;
|
||||
protected final RequestDelegate<T> delegate;
|
||||
|
||||
/**
|
||||
* Create a delegate for an un-authenticated resource.
|
||||
*/
|
||||
public ResourceDelegate(final Resource resource, final String secret, final RequestDelegate<T> 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<RegisterUserAgentResponse> delegate) {
|
||||
BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "registration"));
|
||||
} catch (URISyntaxException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<RegisterUserAgentResponse>(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<Void> delegate) {
|
||||
final BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "registration/" + uaid));
|
||||
} catch (Exception e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<Void>(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<SubscribeChannelResponse> 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<SubscribeChannelResponse>(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<Void> 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<Void>(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<Void> delegate) {
|
||||
final BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "registration/" + uaid));
|
||||
} catch (Exception e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) {
|
||||
@Override
|
||||
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
|
||||
delegate.handleSuccess(null);
|
||||
}
|
||||
};
|
||||
|
||||
resource.delete();
|
||||
}
|
||||
}
|
@ -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 "<AutopushClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 <b>live</b> 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> T assertSuccess(RequestDelegate<T> delegate, Class<T> klass) {
|
||||
verify(delegate, never()).handleError(any(Exception.class));
|
||||
verify(delegate, never()).handleFailure(any(AutopushClientException.class));
|
||||
|
||||
final ArgumentCaptor<T> register = ArgumentCaptor.forClass(klass);
|
||||
verify(delegate).handleSuccess(register.capture());
|
||||
|
||||
return register.getValue();
|
||||
}
|
||||
|
||||
protected <T> AutopushClientException assertFailure(RequestDelegate<T> delegate, Class<T> klass) {
|
||||
verify(delegate, never()).handleError(any(Exception.class));
|
||||
verify(delegate, never()).handleSuccess(any(klass));
|
||||
|
||||
final ArgumentCaptor<AutopushClientException> failure = ArgumentCaptor.forClass(AutopushClientException.class);
|
||||
verify(delegate).handleFailure(failure.capture());
|
||||
|
||||
return failure.getValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserAgent() throws Exception {
|
||||
final RequestDelegate<RegisterUserAgentResponse> 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<Void> reregisterDelegate = mock(RequestDelegate.class);
|
||||
client.reregisterUserAgent(registerResponse.uaid, registerResponse.secret, Utils.generateGuid(), reregisterDelegate);
|
||||
|
||||
Assert.assertNull(assertSuccess(reregisterDelegate, Void.class));
|
||||
|
||||
// Unregistering should succeed.
|
||||
final RequestDelegate<Void> 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<Void> 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<RegisterUserAgentResponse> 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<SubscribeChannelResponse> 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<Void> 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<Void> 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<Void> 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());
|
||||
}
|
||||
}
|
@ -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 <runxpcshelltests.py>.
|
||||
_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 {
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ this.DownloadImport.prototype = {
|
||||
yield this.list.add(download);
|
||||
|
||||
if (resumeDownload) {
|
||||
download.start();
|
||||
download.start().catch(() => {});
|
||||
} else {
|
||||
yield download.refresh();
|
||||
}
|
||||
|
@ -1080,7 +1080,7 @@ this.DownloadObserver = {
|
||||
this._wakeTimer = null;
|
||||
|
||||
for (let download of this._canceledOfflineDownloads) {
|
||||
download.start();
|
||||
download.start().catch(() => {});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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]);
|
||||
}
|
||||
},
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
61
toolkit/components/telemetry/docs/heartbeat-ping.rst
Normal file
61
toolkit/components/telemetry/docs/heartbeat-ping.rst
Normal file
@ -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: <UUID>,
|
||||
environment: { ... }
|
||||
... common ping data ...
|
||||
payload: {
|
||||
version: 1,
|
||||
flowId: <string>,
|
||||
... timestamps below ...
|
||||
offeredTS: <integer epoch timestamp>,
|
||||
learnMoreTS: <integer epoch timestamp>,
|
||||
votedTS: <integer epoch timestamp>,
|
||||
engagedTS: <integer epoch timestamp>,
|
||||
closedTS: <integer epoch timestamp>,
|
||||
expiredTS: <integer epoch timestamp>,
|
||||
windowClosedTS: <integer epoch timestamp>,
|
||||
... user's rating below ...
|
||||
score: <integer>,
|
||||
... survey meta fields below ...
|
||||
surveyId: <string>,
|
||||
surveyVersion: <integer>,
|
||||
testing: <boolean>
|
||||
}
|
||||
}
|
||||
|
||||
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 <common-ping>`
|
||||
|
@ -23,5 +23,6 @@ Client-side, this consists of:
|
||||
deletion-ping
|
||||
crash-ping
|
||||
uitour-ping
|
||||
heartbeat-ping
|
||||
preferences
|
||||
crashes
|
||||
|
@ -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 <deletion-ping>` - sent when FHR upload is disabled, requesting deletion of the data associated with this user
|
||||
|
||||
Archiving
|
||||
|
@ -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 += [
|
||||
|
241
toolkit/modules/tests/PromiseTestUtils.jsm
Normal file
241
toolkit/modules/tests/PromiseTestUtils.jsm
Normal file
@ -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.");
|
||||
}
|
||||
},
|
||||
};
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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];
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user