Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-08-16 17:18:48 -04:00
commit 23d518058d
70 changed files with 2403 additions and 994 deletions

View File

@ -1582,12 +1582,22 @@ pref("loop.retry_delay.start", 60000);
pref("loop.retry_delay.limit", 300000);
pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
pref("loop.feedback.product", "Loop");
pref("loop.debug.websocket", false);
// serverURL to be assigned by services team
pref("services.push.serverURL", "wss://push.services.mozilla.com/");
pref("social.sidebar.unload_timeout_ms", 10000);
// activation from inside of share panel is possible if activationPanelEnabled
// is true. Pref'd off for release while usage testing is done through beta.
#ifdef RELEASE_BUILD
pref("social.share.activationPanelEnabled", false);
#else
pref("social.share.activationPanelEnabled", true);
#endif
pref("social.shareDirectory", "https://activations.cdn.mozilla.net/en-US/sharePanel.html");
pref("dom.identity.enabled", false);
// Block insecure active content on https pages

View File

@ -43,12 +43,17 @@
function parseQueryString() {
let url = document.documentURI;
let queryString = url.replace(/^about:socialerror\??/, "");
var searchParams = new URLSearchParams(url);
let modeMatch = queryString.match(/mode=([^&]+)/);
let mode = modeMatch && modeMatch[1] ? modeMatch[1] : "";
let originMatch = queryString.match(/origin=([^&]+)/);
config.origin = originMatch && originMatch[1] ? decodeURIComponent(originMatch[1]) : "";
let mode = searchParams.get("mode");
config.directory = searchParams.get("directory");
config.origin = searchParams.get("origin");
let encodedURL = searchParams.get("url");
let url = decodeURIComponent(encodedURL);
if (config.directory) {
let URI = Services.io.newURI(url, null, null);
config.origin = Services.scriptSecurityManager.getNoAppCodebasePrincipal(URI).origin;
}
switch (mode) {
case "compactInfo":
@ -59,10 +64,6 @@
document.getElementById("btnCloseSidebar").style.display = 'none';
//intentional fall-through
case "tryAgain":
let urlMatch = queryString.match(/url=([^&]+)/);
let encodedURL = urlMatch && urlMatch[1] ? urlMatch[1] : "";
let url = decodeURIComponent(encodedURL);
config.tryAgainCallback = loadQueryURL;
config.queryURL = url;
break;
@ -80,7 +81,7 @@
let productName = brandBundle.GetStringFromName("brandShortName");
let provider = Social._getProviderFromOrigin(config.origin);
let providerName = provider && provider.name;
let providerName = provider ? provider.name : config.origin;
// Sets up the error message
let msg = browserBundle.formatStringFromName("social.error.message", [productName, providerName], 2);

View File

@ -62,11 +62,6 @@ let gDataNotificationInfoBar = {
accessKey: gNavigatorBundle.getString("dataReportingNotification.button.accessKey"),
popup: null,
callback: function () {
// Clicking the button to go to the preferences tab constitutes
// acceptance of the data upload policy for Firefox Health Report.
// This will ensure the checkbox is checked. The user has the option of
// unchecking it.
request.onUserAccept("info-bar-button-pressed");
this._actionTaken = true;
window.openAdvancedPreferences("dataChoicesTab");
}.bind(this),
@ -81,16 +76,14 @@ let gDataNotificationInfoBar = {
buttons,
function onEvent(event) {
if (event == "removed") {
if (!this._actionTaken) {
request.onUserAccept("info-bar-dismissed");
}
Services.obs.notifyObservers(null, "datareporting:notify-data-policy:close", null);
}
}.bind(this)
);
// Tell the notification request we have displayed the notification.
// It is important to defer calling onUserNotifyComplete() until we're
// actually sure the notification was displayed. If we ever called
// onUserNotifyComplete() without showing anything to the user, that
// would be very good for user choice. It may also have legal impact.
request.onUserNotifyComplete();
},
@ -102,18 +95,16 @@ let gDataNotificationInfoBar = {
}
},
onNotifyDataPolicy: function (request) {
try {
this._displayDataPolicyInfoBar(request);
} catch (ex) {
request.onUserNotifyFailed(ex);
}
},
observe: function(subject, topic, data) {
switch (topic) {
case "datareporting:notify-data-policy:request":
this.onNotifyDataPolicy(subject.wrappedJSObject.object);
let request = subject.wrappedJSObject.object;
try {
this._displayDataPolicyInfoBar(request);
} catch (ex) {
request.onUserNotifyFailed(ex);
return;
}
break;
case "datareporting:notify-data-policy:close":

View File

@ -119,7 +119,7 @@
#endif
<command id="History:UndoCloseTab" oncommand="undoCloseTab();"/>
<command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
<command id="Social:SharePage" oncommand="SocialShare.sharePage();" disabled="true"/>
<command id="Social:SharePage" oncommand="SocialShare.sharePage();"/>
<command id="Social:ToggleSidebar" oncommand="SocialSidebar.toggleSidebar();" hidden="true"/>
<command id="Social:ToggleNotifications" oncommand="Social.toggleNotifications();" hidden="true"/>
<command id="Social:Addons" oncommand="BrowserOpenAddonsMgr('addons://list/service');"/>

View File

@ -183,7 +183,7 @@ SocialUI = {
// about:home or the share panel, we bypass the enable prompt. Any website
// activation, such as from the activations directory or a providers website
// will still get the prompt.
_activationEventHandler: function SocialUI_activationHandler(e, aBypassUserEnable=false) {
_activationEventHandler: function SocialUI_activationHandler(e, options={}) {
let targetDoc;
let node;
if (e.target instanceof HTMLDocument) {
@ -197,7 +197,9 @@ SocialUI = {
if (!(targetDoc instanceof HTMLDocument))
return;
if (!aBypassUserEnable && targetDoc.defaultView != content)
// The share panel iframe will not match "content" so it passes a bypass
// flag
if (!options.bypassContentCheck && targetDoc.defaultView != content)
return;
// If we are in PB mode, we silently do nothing (bug 829404 exists to
@ -233,11 +235,25 @@ SocialUI = {
if (provider.sidebarURL) {
SocialSidebar.show(provider.origin);
}
if (provider.shareURL) {
// make this new provider the selected provider. If the panel hasn't
// been opened, we need to make the frame first.
SocialShare._createFrame();
SocialShare.iframe.setAttribute('src', 'data:text/plain;charset=utf8,');
SocialShare.iframe.setAttribute('origin', provider.origin);
// get the right button selected
SocialShare.populateProviderMenu();
if (SocialShare.panel.state == "open") {
SocialShare.sharePage(provider.origin);
}
}
if (provider.postActivationURL) {
openUILinkIn(provider.postActivationURL, "tab");
// if activated from an open share panel, we load the landing page in
// a background tab
gBrowser.loadOneTab(provider.postActivationURL, {inBackground: SocialShare.panel.state == "open"});
}
});
}, aBypassUserEnable);
}, options);
},
showLearnMore: function() {
@ -290,10 +306,10 @@ SocialUI = {
// called on tab/urlbar/location changes and after customization. Update
// anything that is tab specific.
updateState: function() {
SocialShare.update();
if (!SocialUI.enabled)
return;
SocialMarks.update();
SocialShare.update();
}
}
@ -434,6 +450,12 @@ SocialFlyout = {
}
SocialShare = {
get _dynamicResizer() {
delete this._dynamicResizer;
this._dynamicResizer = new DynamicResizeWatcher();
return this._dynamicResizer;
},
// Share panel may be attached to the overflow or menu button depending on
// customization, we need to manage open state of the anchor.
get anchor() {
@ -452,15 +474,27 @@ SocialShare = {
return this.panel.lastChild;
},
get activationPanelEnabled () {
// ability to pref off for release
return Services.prefs.getBoolPref("social.share.activationPanelEnabled");
},
_activationHandler: function(event) {
if (!SocialShare.activationPanelEnabled)
return;
SocialUI._activationEventHandler(event, { bypassContentCheck: true, bypassInstallPanel: true });
},
uninit: function () {
if (this.iframe) {
this.iframe.removeEventListener("ActivateSocialFeature", this._activationHandler, true, true);
this.iframe.remove();
}
},
_createFrame: function() {
let panel = this.panel;
if (!SocialUI.enabled || this.iframe)
if (this.iframe)
return;
this.panel.hidden = false;
// create and initialize the panel for this window
@ -472,6 +506,7 @@ SocialShare = {
iframe.setAttribute("disableglobalhistory", "true");
iframe.setAttribute("flex", "1");
panel.appendChild(iframe);
this.iframe.addEventListener("ActivateSocialFeature", this._activationHandler, true, true);
this.populateProviderMenu();
},
@ -481,11 +516,19 @@ SocialShare = {
if (lastProviderOrigin) {
provider = Social._getProviderFromOrigin(lastProviderOrigin);
}
// if we are able to activate a provider we don't need to do anything fancy
// here, the user will land on the activation panel if no previously
// selected provider is available.
if (this.activationPanelEnabled)
return provider;
// if they have a provider selected in the sidebar use that for the initial
// default in share
if (!provider)
provider = SocialSidebar.provider;
// if our provider has no shareURL, select the first one that does
// if our provider has no shareURL, select the first one that does. If we
// have no selected provider and activation is available, default to that
// panel.
if (!provider || !provider.shareURL) {
let providers = [p for (p of Social.providers) if (p.shareURL)];
provider = providers.length > 0 && providers[0];
@ -498,17 +541,12 @@ SocialShare = {
return;
let providers = [p for (p of Social.providers) if (p.shareURL)];
let hbox = document.getElementById("social-share-provider-buttons");
// selectable providers are inserted before the provider-menu seperator,
// remove any menuitems in that area
while (hbox.firstChild) {
// remove everything before the add-share-provider button (which should also
// be lastChild if any share providers were added)
let addButton = document.getElementById("add-share-provider");
while (hbox.firstChild != addButton) {
hbox.removeChild(hbox.firstChild);
}
// reset our share toolbar
// only show a selection if there is more than one
if (!SocialUI.enabled || providers.length < 2) {
this.panel.firstChild.hidden = true;
return;
}
let selectedProvider = this.getSelectedProvider();
for (let provider of providers) {
let button = document.createElement("toolbarbutton");
@ -518,17 +556,16 @@ SocialShare = {
button.setAttribute("image", provider.iconURL);
button.setAttribute("tooltiptext", provider.name);
button.setAttribute("origin", provider.origin);
button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin')); this.checked=true;");
button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));");
if (provider == selectedProvider) {
this.defaultButton = button;
}
hbox.appendChild(button);
hbox.insertBefore(button, addButton);
}
if (!this.defaultButton) {
this.defaultButton = hbox.firstChild
this.defaultButton = this.activationPanelEnabled ? addButton : hbox.firstChild;
}
this.defaultButton.setAttribute("checked", "true");
this.panel.firstChild.hidden = false;
},
get shareButton() {
@ -560,8 +597,8 @@ SocialShare = {
let shareButton = widget.forWindow(window).node;
// hidden state is based on available share providers and location of
// button. It's always visible and disabled in the customization palette.
shareButton.hidden = !SocialUI.enabled || (widget.areaType &&
[p for (p of Social.providers) if (p.shareURL)].length == 0);
shareButton.hidden = !this.activationPanelEnabled && (!SocialUI.enabled || (widget.areaType &&
[p for (p of Social.providers) if (p.shareURL)].length == 0));
let disabled = !widget.areaType || shareButton.hidden || !this.canSharePage(gBrowser.currentURI);
// 1. update the relevent command's disabled state so the keyboard
@ -577,6 +614,9 @@ SocialShare = {
cmd.removeAttribute("disabled");
shareButton.removeAttribute("disabled");
}
// enable or disable the activation panel
document.getElementById("add-share-provider").hidden = !this.activationPanelEnabled;
},
_onclick: function() {
@ -608,10 +648,15 @@ SocialShare = {
if (!iframe)
return;
iframe.removeAttribute("src");
iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
encodeURIComponent(iframe.getAttribute("origin")),
null, null, null, null);
let url;
let origin = iframe.getAttribute("origin");
if (!origin && this.activationPanelEnabled) {
// directory site is down
url = "about:socialerror?mode=tryAgainOnly&directory=1&url=" + encodeURIComponent(iframe.getAttribute("src"));
} else {
url = "about:socialerror?mode=compactInfo&origin=" + encodeURIComponent(origin);
}
iframe.webNavigation.loadURI(url, null, null, null, null);
sizeSocialPanelToContent(this.panel, iframe);
},
@ -621,13 +666,6 @@ SocialShare = {
// will call sharePage with an origin for us to switch to.
this._createFrame();
let iframe = this.iframe;
let provider;
if (providerOrigin)
provider = Social._getProviderFromOrigin(providerOrigin);
else
provider = this.getSelectedProvider();
if (!provider || !provider.shareURL)
return;
// graphData is an optional param that either defines the full set of data
// to be shared, or partial data about the current page. It is set by a call
@ -659,20 +697,25 @@ SocialShare = {
}
this.currentShare = pageData;
let provider;
if (providerOrigin)
provider = Social._getProviderFromOrigin(providerOrigin);
else
provider = this.getSelectedProvider();
if (!provider || !provider.shareURL) {
this.showDirectory();
return;
}
// check the menu button
let hbox = document.getElementById("social-share-provider-buttons");
let btn = hbox.querySelector("[origin='" + provider.origin + "']");
btn.checked = true;
let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData);
let size = provider.getPageSize("share");
if (size) {
if (this._dynamicResizer) {
this._dynamicResizer.stop();
this._dynamicResizer = null;
}
let {width, height} = size;
width += this.panel.boxObject.width - iframe.boxObject.width;
height += this.panel.boxObject.height - iframe.boxObject.height;
this.panel.sizeTo(width, height);
} else {
this._dynamicResizer = new DynamicResizeWatcher();
this._dynamicResizer.stop();
}
// if we've already loaded this provider/page share endpoint, we don't want
@ -684,7 +727,7 @@ SocialShare = {
reload = shareEndpoint != iframe.contentDocument.location.spec;
}
if (!reload) {
if (this._dynamicResizer)
if (!size)
this._dynamicResizer.start(this.panel, iframe);
iframe.docShell.isActive = true;
iframe.docShell.isAppTab = true;
@ -702,7 +745,13 @@ SocialShare = {
// should close the window when done.
iframe.contentWindow.opener = iframe.contentWindow;
setTimeout(function() {
if (SocialShare._dynamicResizer) { // may go null if hidden quickly
if (size) {
let panel = SocialShare.panel;
let {width, height} = size;
width += panel.boxObject.width - iframe.boxObject.width;
height += panel.boxObject.height - iframe.boxObject.height;
panel.sizeTo(width, height);
} else {
SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
}
}, 0);
@ -723,10 +772,32 @@ SocialShare = {
let uri = Services.io.newURI(shareEndpoint, null, null);
iframe.setAttribute("origin", provider.origin);
iframe.setAttribute("src", shareEndpoint);
this._openPanel();
},
showDirectory: function() {
let url = Services.prefs.getCharPref("social.shareDirectory");
this._createFrame();
let iframe = this.iframe;
iframe.removeAttribute("origin");
iframe.setAttribute("src", url);
iframe.addEventListener("load", function panelBrowserOnload(e) {
iframe.removeEventListener("load", panelBrowserOnload, true);
SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
iframe.addEventListener("unload", function panelBrowserOnload(e) {
iframe.removeEventListener("unload", panelBrowserOnload, true);
SocialShare._dynamicResizer.stop();
}, true);
}, true);
this._openPanel();
},
_openPanel: function() {
let anchor = document.getAnonymousElementByAttribute(this.anchor, "class", "toolbarbutton-icon");
this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
Social.setErrorListener(iframe, this.setErrorMessage.bind(this));
Social.setErrorListener(this.iframe, this.setErrorMessage.bind(this));
Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(0);
}
};

View File

@ -246,7 +246,11 @@
onpopuphidden="SocialShare.onHidden()"
hidden="true">
<vbox class="social-share-toolbar">
<arrowscrollbox id="social-share-provider-buttons" orient="vertical" flex="1"/>
<arrowscrollbox id="social-share-provider-buttons" orient="vertical" flex="1">
<toolbarbutton id="add-share-provider" class="toolbarbutton share-provider-button" type="radio"
group="share-providers" tooltiptext="&findShareServices.label;"
oncommand="SocialShare.showDirectory()"/>
</arrowscrollbox>
</vbox>
</panel>

View File

@ -334,7 +334,7 @@ nsContextMenu.prototype = {
let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial;
let pageShare = shareEnabled && !(this.isContentSelected ||
this.onTextInput || this.onLink || this.onImage ||
this.onVideo || this.onAudio);
this.onVideo || this.onAudio || this.onCanvas);
this.showItem("context-sharepage", pageShare);
this.showItem("context-shareselect", shareEnabled && this.isContentSelected);
this.showItem("context-sharelink", shareEnabled && (this.onLink || this.onPlainTextLink) && !this.onMailtoLink);

View File

@ -2,30 +2,49 @@
* 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/. */
let originalPolicy = null;
/**
* Display a datareporting notification to the user.
*
* @param {String} name
*/
function sendNotifyRequest(name) {
let ns = {};
Components.utils.import("resource://gre/modules/services/datareporting/policy.jsm", ns);
Components.utils.import("resource://gre/modules/Preferences.jsm", ns);
Cu.import("resource://gre/modules/services/datareporting/policy.jsm", ns);
Cu.import("resource://gre/modules/Preferences.jsm", ns);
let service = Components.classes["@mozilla.org/datareporting/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
let service = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
ok(service.healthReporter, "Health Reporter instance is available.");
Cu.import("resource://gre/modules/Promise.jsm", ns);
let deferred = ns.Promise.defer();
if (!originalPolicy) {
originalPolicy = service.policy;
}
let policyPrefs = new ns.Preferences("testing." + name + ".");
ok(service._prefs, "Health Reporter prefs are available.");
let hrPrefs = service._prefs;
let policy = new ns.DataReportingPolicy(policyPrefs, hrPrefs, service);
policy.dataSubmissionPolicyBypassNotification = false;
service.policy = policy;
policy.firstRunDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
is(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED, "Policy is in unnotified state.");
service.healthReporter.onInit().then(function onSuccess () {
is(policy.ensureUserNotified(), false, "User not notified about data policy on init.");
ok(policy._userNotifyPromise, "_userNotifyPromise defined.");
policy._userNotifyPromise.then(
deferred.resolve.bind(deferred),
deferred.reject.bind(deferred)
);
}.bind(this), deferred.reject.bind(deferred));
service.healthReporter.onInit().then(function onInit() {
is(policy.ensureNotifyResponse(new Date()), false, "User has not responded to policy.");
});
return policy;
return [policy, deferred.promise];
}
/**
@ -55,6 +74,7 @@ function waitForNotificationClose(notification, cb) {
let dumpAppender, rootLogger;
function test() {
registerCleanupFunction(cleanup);
waitForExplicitFinish();
let ns = {};
@ -64,29 +84,41 @@ function test() {
dumpAppender.level = ns.Log.Level.All;
rootLogger.addAppender(dumpAppender);
let notification = document.getElementById("global-notificationbox");
let policy;
closeAllNotifications().then(function onSuccess () {
let notification = document.getElementById("global-notificationbox");
notification.addEventListener("AlertActive", function active() {
notification.removeEventListener("AlertActive", active, true);
notification.addEventListener("AlertActive", function active() {
notification.removeEventListener("AlertActive", active, true);
is(notification.allNotifications.length, 1, "Notification Displayed.");
executeSoon(function afterNotification() {
is(policy.notifyState, policy.STATE_NOTIFY_WAIT, "Policy is waiting for user response.");
ok(!policy.dataSubmissionPolicyAccepted, "Data submission policy not yet accepted.");
waitForNotificationClose(notification.currentNotification, function onClose() {
is(policy.notifyState, policy.STATE_NOTIFY_COMPLETE, "Closing info bar completes user notification.");
ok(policy.dataSubmissionPolicyAccepted, "Data submission policy accepted.");
is(policy.dataSubmissionPolicyResponseType, "accepted-info-bar-dismissed",
"Reason for acceptance was info bar dismissal.");
is(notification.allNotifications.length, 0, "No notifications remain.");
test_multiple_windows();
executeSoon(function afterNotification() {
waitForNotificationClose(notification.currentNotification, function onClose() {
is(notification.allNotifications.length, 0, "No notifications remain.");
is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Version pref set.");
ok(policy.dataSubmissionPolicyNotifiedDate.getTime() > -1, "Date pref set.");
test_multiple_windows();
});
notification.currentNotification.close();
});
notification.currentNotification.close();
});
}, true);
}, true);
policy = sendNotifyRequest("single_window_notified");
let [policy, promise] = sendNotifyRequest("single_window_notified");
is(policy.dataSubmissionPolicyAcceptedVersion, 0, "No version should be set on init.");
is(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0, "No date should be set on init.");
is(policy.userNotifiedOfCurrentPolicy, false, "User not notified about datareporting policy.");
promise.then(function () {
is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Policy version set.");
is(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0, true, "Policy date set.");
is(policy.userNotifiedOfCurrentPolicy, true, "User notified about datareporting policy.");
}.bind(this), function (err) {
throw err;
});
}.bind(this), function onError (err) {
throw err;
});
}
function test_multiple_windows() {
@ -98,7 +130,7 @@ function test_multiple_windows() {
let notification2 = window2.document.getElementById("global-notificationbox");
ok(notification2, "2nd window has a global notification box.");
let policy;
let [policy, promise] = sendNotifyRequest("multiple_window_behavior");
let displayCount = 0;
let prefWindowClosed = false;
let mutationObserversRemoved = false;
@ -129,8 +161,8 @@ function test_multiple_windows() {
dump("Finishing multiple window test.\n");
rootLogger.removeAppender(dumpAppender);
delete dumpAppender;
delete rootLogger;
dumpAppender = null;
rootLogger = null;
finish();
}
let closeCount = 0;
@ -143,12 +175,8 @@ function test_multiple_windows() {
}
ok(true, "Closing info bar on one window closed them on all.");
is(policy.userNotifiedOfCurrentPolicy, true, "Data submission policy accepted.");
is(policy.notifyState, policy.STATE_NOTIFY_COMPLETE,
"Closing info bar with multiple windows completes notification.");
ok(policy.dataSubmissionPolicyAccepted, "Data submission policy accepted.");
is(policy.dataSubmissionPolicyResponseType, "accepted-info-bar-button-pressed",
"Policy records reason for acceptance was button press.");
is(notification1.allNotifications.length, 0, "No notifications remain on main window.");
is(notification2.allNotifications.length, 0, "No notifications remain on 2nd window.");
@ -192,7 +220,20 @@ function test_multiple_windows() {
executeSoon(onAlertDisplayed);
}, true);
policy = sendNotifyRequest("multiple_window_behavior");
promise.then(null, function onError(err) {
throw err;
});
});
}
function cleanup () {
// In case some test fails.
if (originalPolicy) {
let service = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
service.policy = originalPolicy;
}
return closeAllNotifications();
}

View File

@ -51,9 +51,11 @@ function test() {
is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded");
info("Actual URI: " + req.URI.spec);
req.cancel(Components.results.NS_ERROR_FAILURE);
executeSoon(nextTest);
}
}
};
gBrowser.addProgressListener(listener);
registerCleanupFunction(function () {

View File

@ -7,6 +7,26 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
function closeAllNotifications () {
let notificationBox = document.getElementById("global-notificationbox");
if (!notificationBox || !notificationBox.currentNotification) {
return Promise.resolve();
}
let deferred = Promise.defer();
for (let notification of notificationBox.allNotifications) {
waitForNotificationClose(notification, function () {
if (notificationBox.allNotifications.length === 0) {
deferred.resolve();
}
});
notification.close();
}
return deferred.promise;
}
function whenDelayedStartupFinished(aWindow, aCallback) {
Services.obs.addObserver(function observer(aSubject, aTopic) {
if (aWindow == aSubject) {

View File

@ -89,6 +89,7 @@ function runTest(testNum) {
},
function () {
info("context menu for text");
// Context menu for plain text
plainTextItems = ["context-navigation", null,
["context-back", false,
@ -96,6 +97,7 @@ function runTest(testNum) {
"context-reload", true,
"context-bookmarkpage", true], null,
"---", null,
"context-sharepage", true,
"context-savepage", true,
"---", null,
"context-viewbgimage", false,
@ -110,6 +112,7 @@ function runTest(testNum) {
},
function () {
info("context menu for text link");
// Context menu for text link
if (perWindowPrivateBrowsing) {
checkContextMenu(["context-openlinkintab", true,
@ -117,6 +120,7 @@ function runTest(testNum) {
"context-openlinkprivate", true,
"---", null,
"context-bookmarklink", true,
"context-sharelink", true,
"context-savelink", true,
"context-copylink", true,
"context-searchselect", true
@ -126,6 +130,7 @@ function runTest(testNum) {
"context-openlink", true,
"---", null,
"context-bookmarklink", true,
"context-sharelink", true,
"context-savelink", true,
"context-copylink", true,
"context-searchselect", true
@ -136,6 +141,7 @@ function runTest(testNum) {
},
function () {
info("context menu for mailto link");
// Context menu for text mailto-link
checkContextMenu(["context-copyemail", true,
"context-searchselect", true
@ -145,12 +151,14 @@ function runTest(testNum) {
},
function () {
info("context menu for image");
// Context menu for an image
checkContextMenu(["context-viewimage", true,
"context-copyimage-contents", true,
"context-copyimage", true,
"---", null,
"context-saveimage", true,
"context-shareimage", true,
"context-sendimage", true,
"context-setDesktopBackground", true,
"context-viewimageinfo", true
@ -160,6 +168,7 @@ function runTest(testNum) {
},
function () {
info("context menu for canvas");
// Context menu for a canvas
checkContextMenu(["context-viewimage", true,
"context-saveimage", true,
@ -170,6 +179,7 @@ function runTest(testNum) {
},
function () {
info("context menu for video_ok");
// Context menu for a video (with a VALID media source)
checkContextMenu(["context-media-play", true,
"context-media-mute", true,
@ -186,6 +196,7 @@ function runTest(testNum) {
"context-copyvideourl", true,
"---", null,
"context-savevideo", true,
"context-sharevideo", true,
"context-video-saveimage", true,
"context-sendvideo", true
].concat(inspectItems));
@ -194,6 +205,7 @@ function runTest(testNum) {
},
function () {
info("context menu for audio_in_video");
// Context menu for a video (with an audio-only file)
checkContextMenu(["context-media-play", true,
"context-media-mute", true,
@ -214,6 +226,7 @@ function runTest(testNum) {
},
function () {
info("context menu for video_bad");
// Context menu for a video (with an INVALID media source)
checkContextMenu(["context-media-play", false,
"context-media-mute", false,
@ -230,6 +243,7 @@ function runTest(testNum) {
"context-copyvideourl", true,
"---", null,
"context-savevideo", true,
"context-sharevideo", true,
"context-video-saveimage", false,
"context-sendvideo", true
].concat(inspectItems));
@ -238,6 +252,7 @@ function runTest(testNum) {
},
function () {
info("context menu for video_bad2");
// Context menu for a video (with an INVALID media source)
checkContextMenu(["context-media-play", false,
"context-media-mute", false,
@ -254,6 +269,7 @@ function runTest(testNum) {
"context-copyvideourl", false,
"---", null,
"context-savevideo", false,
"context-sharevideo", false,
"context-video-saveimage", false,
"context-sendvideo", false
].concat(inspectItems));
@ -262,6 +278,7 @@ function runTest(testNum) {
},
function () {
info("context menu for iframe");
// Context menu for an iframe
checkContextMenu(["context-navigation", null,
["context-back", false,
@ -269,6 +286,7 @@ function runTest(testNum) {
"context-reload", true,
"context-bookmarkpage", true], null,
"---", null,
"context-sharepage", true,
"context-savepage", true,
"---", null,
"context-viewbgimage", false,
@ -296,6 +314,7 @@ function runTest(testNum) {
},
function () {
info("context menu for video_in_iframe");
// Context menu for a video in an iframe
checkContextMenu(["context-media-play", true,
"context-media-mute", true,
@ -312,6 +331,7 @@ function runTest(testNum) {
"context-copyvideourl", true,
"---", null,
"context-savevideo", true,
"context-sharevideo", true,
"context-video-saveimage", true,
"context-sendvideo", true,
"frame", null,
@ -332,12 +352,14 @@ function runTest(testNum) {
},
function () {
info("context menu for image_in_iframe");
// Context menu for an image in an iframe
checkContextMenu(["context-viewimage", true,
"context-copyimage-contents", true,
"context-copyimage", true,
"---", null,
"context-saveimage", true,
"context-shareimage", true,
"context-sendimage", true,
"context-setDesktopBackground", true,
"context-viewimageinfo", true,
@ -359,6 +381,7 @@ function runTest(testNum) {
},
function () {
info("context menu for textarea");
// Context menu for textarea before spell check initialization finishes
checkContextMenu(["context-undo", false,
"---", null,
@ -376,6 +399,7 @@ function runTest(testNum) {
},
function () {
info("context menu for textarea, wait for spell check");
// Context menu for textarea after spell check initialization finishes
checkContextMenu(["*chubbiness", true, // spelling suggestion
"spell-add-to-dictionary", true,
@ -401,6 +425,7 @@ function runTest(testNum) {
},
function () {
info("context menu for text");
// Re-check context menu for plain text to make sure it hasn't changed
checkContextMenu(plainTextItems);
closeContextMenu();
@ -408,6 +433,7 @@ function runTest(testNum) {
},
function () {
info("context menu for textarea after word added");
// Context menu for textarea after a word has been added
// to the dictionary
checkContextMenu(["spell-undo-add-to-dictionary", true,
@ -433,6 +459,7 @@ function runTest(testNum) {
},
function () {
info("context menu for contenteditable");
// Context menu for contenteditable
checkContextMenu(["spell-no-suggestions", false,
"spell-add-to-dictionary", true,
@ -458,12 +485,14 @@ function runTest(testNum) {
},
function () {
info("context menu for link");
executeCopyCommand("cmd_copyLink", "http://mozilla.com/");
closeContextMenu();
openContextMenuFor(pagemenu); // Invoke context menu for next test.
},
function () {
info("context menu for pagemenu");
// Context menu for element with assigned content context menu
checkContextMenu(["context-navigation", null,
["context-back", false,
@ -491,6 +520,7 @@ function runTest(testNum) {
"---", null,
"+Checkbox", {type: "checkbox", icon: "", checked: false, disabled: false}], null,
"---", null,
"context-sharepage", true,
"context-savepage", true,
"---", null,
"context-viewbgimage", false,
@ -516,6 +546,7 @@ function runTest(testNum) {
},
function () {
info("context menu for fullscreen mode");
// Context menu for DOM Fullscreen mode (NOTE: this is *NOT* on an img)
checkContextMenu(["context-navigation", null,
["context-back", false,
@ -525,6 +556,7 @@ function runTest(testNum) {
"---", null,
"context-leave-dom-fullscreen", true,
"---", null,
"context-sharepage", true,
"context-savepage", true,
"---", null,
"context-viewbgimage", false,
@ -546,6 +578,7 @@ function runTest(testNum) {
},
function () {
info("context menu for element with assigned content context menu");
// Context menu for element with assigned content context menu
// The shift key should bypass content context menu processing
checkContextMenu(["context-navigation", null,
@ -554,6 +587,7 @@ function runTest(testNum) {
"context-reload", true,
"context-bookmarkpage", true], null,
"---", null,
"context-sharepage", true,
"context-savepage", true,
"---", null,
"context-viewbgimage", false,
@ -568,6 +602,7 @@ function runTest(testNum) {
},
function () {
info("context menu for text selection");
// Context menu for selected text
if (SpecialPowers.Services.appinfo.OS == "Darwin") {
// This test is only enabled on Mac due to bug 736399.
@ -575,6 +610,7 @@ function runTest(testNum) {
"context-selectall", true,
"---", null,
"context-searchselect", true,
"context-shareselect", true,
"context-viewpartialsource-selection", true
].concat(inspectItems));
}
@ -584,6 +620,7 @@ function runTest(testNum) {
},
function () {
info("context menu for text selection with url pattern");
// Context menu for selected text which matches valid URL pattern
if (SpecialPowers.Services.appinfo.OS == "Darwin") {
// This test is only enabled on Mac due to bug 736399.
@ -594,11 +631,13 @@ function runTest(testNum) {
"context-openlinkprivate", true,
"---", null,
"context-bookmarklink", true,
"context-sharelink", true,
"context-savelink", true,
"context-copy", true,
"context-selectall", true,
"---", null,
"context-searchselect", true,
"context-shareselect", true,
"context-viewpartialsource-selection", true
].concat(inspectItems));
} else {
@ -607,11 +646,13 @@ function runTest(testNum) {
"context-openlink", true,
"---", null,
"context-bookmarklink", true,
"context-sharelink", true,
"context-savelink", true,
"context-copy", true,
"context-selectall", true,
"---", null,
"context-searchselect", true,
"context-shareselect", true,
"context-viewpartialsource-selection", true
].concat(inspectItems));
}
@ -624,6 +665,7 @@ function runTest(testNum) {
},
function () {
info("context menu for imagelink");
// Context menu for image link
if (perWindowPrivateBrowsing) {
checkContextMenu(["context-openlinkintab", true,
@ -631,6 +673,7 @@ function runTest(testNum) {
"context-openlinkprivate", true,
"---", null,
"context-bookmarklink", true,
"context-sharelink", true,
"context-savelink", true,
"context-copylink", true,
"---", null,
@ -639,6 +682,7 @@ function runTest(testNum) {
"context-copyimage", true,
"---", null,
"context-saveimage", true,
"context-shareimage", true,
"context-sendimage", true,
"context-setDesktopBackground", true,
"context-viewimageinfo", true
@ -648,6 +692,7 @@ function runTest(testNum) {
"context-openlink", true,
"---", null,
"context-bookmarklink", true,
"context-sharelink", true,
"context-savelink", true,
"context-copylink", true,
"---", null,
@ -656,6 +701,7 @@ function runTest(testNum) {
"context-copyimage", true,
"---", null,
"context-saveimage", true,
"context-shareimage", true,
"context-sendimage", true,
"context-setDesktopBackground", true,
"context-viewimageinfo", true
@ -667,6 +713,7 @@ function runTest(testNum) {
},
function () {
info("context menu for select_inputtext");
// Context menu for selected text in input
checkContextMenu(["context-undo", false,
"---", null,
@ -686,6 +733,7 @@ function runTest(testNum) {
},
function () {
info("context menu for selected text in input[type='password']");
// Context menu for selected text in input[type="password"]
checkContextMenu(["context-undo", false,
"---", null,
@ -709,6 +757,7 @@ function runTest(testNum) {
},
function () {
info("context menu for click-to-play blocked plugin");
// Context menu for click-to-play blocked plugin
checkContextMenu(["context-navigation", null,
["context-back", false,
@ -719,6 +768,7 @@ function runTest(testNum) {
"context-ctp-play", true,
"context-ctp-hide", true,
"---", null,
"context-sharepage", true,
"context-savepage", true,
"---", null,
"context-viewbgimage", false,
@ -734,12 +784,14 @@ function runTest(testNum) {
},
function () {
info("context menu for image with longdesc");
// Context menu for an image with longdesc
checkContextMenu(["context-viewimage", true,
"context-copyimage-contents", true,
"context-copyimage", true,
"---", null,
"context-saveimage", true,
"context-shareimage", true,
"context-sendimage", true,
"context-setDesktopBackground", true,
"context-viewimageinfo", true,
@ -750,6 +802,7 @@ function runTest(testNum) {
},
function () {
info("context menu for iframe with srcdoc attribute set");
// Context menu for an iframe with srcdoc attribute set
checkContextMenu(["context-navigation", null,
["context-back", false,
@ -757,6 +810,7 @@ function runTest(testNum) {
"context-reload", true,
"context-bookmarkpage", true], null,
"---", null,
"context-sharepage", true,
"context-savepage", true,
"---", null,
"context-viewbgimage", false,
@ -779,6 +833,7 @@ function runTest(testNum) {
},
function () {
info("context menu for text input field with spellcheck=false");
// Context menu for text input field with spellcheck=false
checkContextMenu(["context-undo", false,
"---", null,

View File

@ -198,6 +198,7 @@ function runTest(testNum) {
"context-reload", true,
"context-bookmarkpage", true], null,
"---", null,
"context-sharepage", true,
"context-savepage", true,
"---", null,
"context-viewbgimage", false,

View File

@ -11,6 +11,7 @@ support-files =
opengraph/shorturl_linkrel.html
microdata.html
share.html
share_activate.html
social_activate.html
social_activate_iframe.html
social_chat.html

View File

@ -19,7 +19,7 @@ let snippet =
' "iconURL": "chrome://branding/content/icon16.png",' +
' "icon32URL": "chrome://branding/content/favicon32.png",' +
' "icon64URL": "chrome://branding/content/icon64.png",' +
' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",' +
' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
' "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
' };' +
' function activateProvider(node) {' +
@ -41,7 +41,7 @@ let snippet2 =
' "iconURL": "chrome://branding/content/icon16.png",' +
' "icon32URL": "chrome://branding/content/favicon32.png",' +
' "icon64URL": "chrome://branding/content/icon64.png",' +
' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",' +
' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
' "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
' "oneclick": true' +
' };' +

View File

@ -10,10 +10,46 @@ let manifest = { // normal provider
iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png",
shareURL: "https://example.com/browser/browser/base/content/test/social/share.html"
};
let activationPage = "https://example.com/browser/browser/base/content/test/social/share_activate.html";
function waitForProviderEnabled(cb) {
Services.obs.addObserver(function providerSet(subject, topic, data) {
Services.obs.removeObserver(providerSet, "social:provider-enabled");
info("social:provider-enabled observer was notified");
cb();
}, "social:provider-enabled", false);
}
function sendActivationEvent(callback) {
// hack Social.lastEventReceived so we don't hit the "too many events" check.
Social.lastEventReceived = 0;
let doc = SocialShare.iframe.contentDocument;
// if our test has a frame, use it
let button = doc.getElementById("activation");
ok(!!button, "got the activation button");
EventUtils.synthesizeMouseAtCenter(button, {}, doc.defaultView);
if (callback)
executeSoon(callback);
}
function waitForEvent(iframe, eventName, callback) {
iframe.addEventListener(eventName, function load() {
info("page load is "+iframe.contentDocument.location.href);
if (iframe.contentDocument.location.href != "data:text/plain;charset=utf8,") {
iframe.removeEventListener(eventName, load, true);
executeSoon(callback);
}
}, true);
}
function test() {
waitForExplicitFinish();
Services.prefs.setCharPref("social.shareDirectory", activationPage);
registerCleanupFunction(function () {
Services.prefs.clearUserPref("social.directories");
Services.prefs.clearUserPref("social.shareDirectory");
Services.prefs.clearUserPref("social.share.activationPanelEnabled");
});
runSocialTests(tests);
}
@ -75,11 +111,10 @@ let corpus = [
function loadURLInTab(url, callback) {
info("Loading tab with "+url);
let tab = gBrowser.selectedTab = gBrowser.addTab(url);
tab.linkedBrowser.addEventListener("load", function listener() {
waitForEvent(tab.linkedBrowser, "load", () => {
is(tab.linkedBrowser.currentURI.spec, url, "tab loaded")
tab.linkedBrowser.removeEventListener("load", listener, true);
executeSoon(function() { callback(tab) });
}, true);
callback(tab)
});
}
function hasoptions(testOptions, options) {
@ -110,7 +145,6 @@ var tests = {
checkSocialUI();
// share should not be enabled since we only have about:blank page
let shareButton = SocialShare.shareButton;
is(shareButton.disabled, true, "share button is disabled");
// verify the attribute for proper css
is(shareButton.getAttribute("disabled"), "true", "share button attribute is disabled");
// button should be visible
@ -128,7 +162,6 @@ var tests = {
checkSocialUI();
// share should not be enabled since we only have about:blank page
let shareButton = SocialShare.shareButton;
is(shareButton.disabled, false, "share button is enabled");
// verify the attribute for proper css
ok(!shareButton.hasAttribute("disabled"), "share button is enabled");
// button should be visible
@ -149,7 +182,7 @@ var tests = {
function runOneTest() {
loadURLInTab(testData.url, function(tab) {
testTab = tab;
SocialShare.sharePage();
SocialShare.sharePage(manifest.origin);
});
}
@ -241,5 +274,46 @@ var tests = {
SocialShare.sharePage(manifest.origin, null, target);
});
});
},
testSharePanelActivation: function(next) {
let testTab;
// cleared in the cleanup function
Services.prefs.setCharPref("social.directories", "https://example.com");
Services.prefs.setBoolPref("social.share.activationPanelEnabled", true);
// make the iframe so we can wait on the load
SocialShare._createFrame();
let iframe = SocialShare.iframe;
waitForEvent(iframe, "load", () => {
waitForCondition(() => {
// sometimes the iframe is ready before the panel is open, we need to
// wait for both conditions
return SocialShare.panel.state == "open";
}, () => {
is(iframe.contentDocument.location.href, activationPage, "activation page loaded");
waitForProviderEnabled(() => {
let provider = Social._getProviderFromOrigin(manifest.origin);
let port = provider.getWorkerPort();
ok(!!port, "got port");
port.onmessage = function (e) {
let topic = e.data.topic;
info("got topic "+topic+"\n");
switch (topic) {
case "got-share-data-message":
ok(true, "share completed");
gBrowser.removeTab(testTab);
SocialService.uninstallProvider(manifest.origin, next);
break;
}
}
port.postMessage({topic: "test-init"});
});
sendActivationEvent();
}, "share panel did not open and load share page");
});
loadURLInTab(activationPage, function(tab) {
testTab = tab;
SocialShare.sharePage();
});
}
}

View File

@ -34,7 +34,6 @@ function openChat(provider, callback) {
let port = provider.getWorkerPort();
port.onmessage = function(e) {
if (e.data.topic == "got-chatbox-message") {
port.close();
callback();
}
}
@ -42,6 +41,7 @@ function openChat(provider, callback) {
port.postMessage({topic: "test-init"});
port.postMessage({topic: "test-worker-chat", data: url});
gURLsNotRemembered.push(url);
return port;
}
function windowHasChats(win) {
@ -172,6 +172,9 @@ var tests = {
let num = 0;
is(chatbar.childNodes.length, 0, "chatbar starting empty");
is(chatbar.menupopup.childNodes.length, 0, "popup starting empty");
let port = SocialSidebar.provider.getWorkerPort();
ok(port, "provider has a port");
port.postMessage({topic: "test-init"});
makeChat("normal", "first chat", function() {
// got the first one.
@ -195,6 +198,7 @@ var tests = {
chatbar.selectedChat.close();
is(chatbar.selectedChat, second, "second chat is selected");
closeAllChats();
port.close();
next();
});
});
@ -243,24 +247,24 @@ var tests = {
testMultipleProviderChat: function(next) {
// test incomming chats from all providers
openChat(Social.providers[0], function() {
openChat(Social.providers[1], function() {
openChat(Social.providers[2], function() {
let port0 = openChat(Social.providers[0], function() {
let port1 = openChat(Social.providers[1], function() {
let port2 = openChat(Social.providers[2], function() {
let chats = document.getElementById("pinnedchats");
waitForCondition(function() chats.children.length == Social.providers.length,
function() {
ok(true, "one chat window per provider opened");
// test logout of a single provider
let provider = Social.providers[2];
let port = provider.getWorkerPort();
port.postMessage({topic: "test-logout"});
port2.postMessage({topic: "test-logout"});
waitForCondition(function() chats.children.length == Social.providers.length - 1,
function() {
closeAllChats();
waitForCondition(function() chats.children.length == 0,
function() {
ok(!chats.selectedChat, "multiprovider chats are all closed");
port.close();
port0.close();
port1.close();
port2.close();
next();
},
"chat windows didn't close");

View File

@ -36,10 +36,16 @@ function goOnline(callback) {
function openPanel(url, panelCallback, loadCallback) {
// open a flyout
SocialFlyout.open(url, 0, panelCallback);
SocialFlyout.panel.firstChild.addEventListener("load", function panelLoad() {
SocialFlyout.panel.firstChild.removeEventListener("load", panelLoad, true);
loadCallback();
}, true);
// wait for both open and loaded before callback. Since the test doesn't close
// the panel between opens, we cannot rely on events here. We need to ensure
// popupshown happens before we finish out the tests.
waitForCondition(function() {
return SocialFlyout.panel.state == "open" &&
SocialFlyout.iframe.contentDocument.readyState == "complete";
},
loadCallback,
"flyout is open and loaded");
}
function openChat(url, panelCallback, loadCallback) {
@ -183,6 +189,10 @@ var tests = {
// Ensure that the error listener survives the chat window being detached.
let url = "https://example.com/browser/browser/base/content/test/social/social_chat.html";
let panelCallbackCount = 0;
// chatwindow tests throw errors, which muddy test output, if the worker
// doesn't get test-init
let port = SocialSidebar.provider.getWorkerPort();
port.postMessage({topic: "test-init"});
// open a chat while we are still online.
openChat(
url,
@ -200,6 +210,7 @@ var tests = {
waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
function() {
chat.close();
port.close();
next();
},
"error page didn't appear");

View File

@ -0,0 +1,36 @@
<html>
<!-- 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/. -->
<head>
<title>Activation test</title>
</head>
<script>
var data = {
// currently required
"name": "Demo Social Service",
// browser_share.js serves this page from "https://example.com"
"origin": "https://example.com",
"iconURL": "chrome://branding/content/icon16.png",
"icon32URL": "chrome://branding/content/favicon32.png",
"icon64URL": "chrome://branding/content/icon64.png",
"workerURL": "/browser/browser/base/content/test/social/social_worker.js",
"shareURL": "/browser/browser/base/content/test/social/share.html"
}
function activate(node) {
node.setAttribute("data-service", JSON.stringify(data));
var event = new CustomEvent("ActivateSocialFeature");
node.dispatchEvent(event);
}
</script>
<body>
nothing to see here
<button id="activation" onclick="activate(this, true)">Activate the share provider</button>
</body>
</html>

View File

@ -309,8 +309,6 @@ function openLinkIn(url, where, params) {
// result in a new frontmost window (e.g. "javascript:window.open('');").
w.focus();
let newTab;
switch (where) {
case "current":
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
@ -334,29 +332,22 @@ function openLinkIn(url, where, params) {
// fall through
case "tab":
let browser = w.gBrowser;
newTab = browser.loadOneTab(url, {
referrerURI: aReferrerURI,
charset: aCharset,
postData: aPostData,
inBackground: loadInBackground,
allowThirdPartyFixup: aAllowThirdPartyFixup,
relatedToCurrent: aRelatedToCurrent,
skipAnimation: aSkipTabAnimation,
allowMixedContent: aAllowMixedContent });
browser.loadOneTab(url, {
referrerURI: aReferrerURI,
charset: aCharset,
postData: aPostData,
inBackground: loadInBackground,
allowThirdPartyFixup: aAllowThirdPartyFixup,
relatedToCurrent: aRelatedToCurrent,
skipAnimation: aSkipTabAnimation,
allowMixedContent: aAllowMixedContent });
break;
}
w.gBrowser.selectedBrowser.focus();
if (!loadInBackground && w.isBlankPageURL(url)) {
if (newTab) {
// Remote tab content does not focus synchronously, so we set the flag
// on this tab to skip focusing the content if we want to focus the URL
// bar instead.
newTab._urlbarFocused = true;
}
if (!loadInBackground && w.isBlankPageURL(url))
w.focusAndSelectUrlBar();
}
}
// Used as an onclick handler for UI elements with link-like behavior.

View File

@ -166,6 +166,7 @@ let CustomizableUIInternal = {
"preferences-button",
"add-ons-button",
"developer-button",
"social-share-button",
];
if (gPalette.has("switch-to-metro-button")) {
@ -207,7 +208,6 @@ let CustomizableUIInternal = {
"downloads-button",
"home-button",
"loop-call-button",
"social-share-button",
],
defaultCollapsed: false,
}, true);

View File

@ -27,7 +27,7 @@ add_task(function testWrapUnwrap() {
// Creating and destroying a widget should correctly deal with panel placeholders
add_task(function testPanelPlaceholders() {
let panel = document.getElementById(CustomizableUI.AREA_PANEL);
is(panel.querySelectorAll(".panel-customization-placeholder").length, isInWin8() ? 1 : 2, "The number of placeholders should be correct.");
is(panel.querySelectorAll(".panel-customization-placeholder").length, isInWin8() ? 3 : 1, "The number of placeholders should be correct.");
CustomizableUI.createWidget({id: kTestWidget2, label: 'Pretty label', tooltiptext: 'Pretty tooltip', defaultArea: CustomizableUI.AREA_PANEL});
let elem = document.getElementById(kTestWidget2);
let wrapper = document.getElementById("wrapper-" + kTestWidget2);
@ -35,7 +35,7 @@ add_task(function testPanelPlaceholders() {
ok(wrapper, "There should be a wrapper");
is(wrapper.firstChild.id, kTestWidget2, "Wrapper should have test widget");
is(wrapper.parentNode, panel, "Wrapper should be in panel");
is(panel.querySelectorAll(".panel-customization-placeholder").length, isInWin8() ? 3 : 1, "The number of placeholders should be correct.");
is(panel.querySelectorAll(".panel-customization-placeholder").length, isInWin8() ? 2 : 3, "The number of placeholders should be correct.");
CustomizableUI.destroyWidget(kTestWidget2);
wrapper = document.getElementById("wrapper-" + kTestWidget2);
ok(!wrapper, "There should be a wrapper");

View File

@ -22,7 +22,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(zoomControls, printButton);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -48,7 +49,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(zoomControls, savePageButton);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -72,7 +74,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(zoomControls, newWindowButton);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -95,7 +98,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(zoomControls, historyPanelMenu);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -122,7 +126,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(zoomControls, preferencesButton);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -149,7 +154,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterInsert);
simulateItemDrag(openFileButton, zoomControls);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert);
@ -188,7 +194,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterInsert);
simulateItemDrag(openFileButton, editControls);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert);
@ -224,7 +231,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(editControls, zoomControls);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -248,7 +256,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(editControls, newWindowButton);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -275,7 +284,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(editControls, privateBrowsingButton);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -302,7 +312,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(editControls, savePageButton);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -328,7 +339,8 @@ add_task(function() {
"preferences-button",
"add-ons-button",
"edit-controls",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(editControls, panel);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -353,7 +365,8 @@ add_task(function() {
"find-button",
"preferences-button",
"add-ons-button",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
let paletteChildElementCount = palette.childElementCount;
simulateItemDrag(editControls, palette);
@ -377,7 +390,8 @@ add_task(function() {
yield startCustomizing();
let editControls = document.getElementById("edit-controls");
let panel = document.getElementById(CustomizableUI.AREA_PANEL);
let numPlaceholders = isInWin8() ? 1 : 2;
let numPlaceholders = isInWin8() ? 3 : 1;
is(numPlaceholders, panel.getElementsByClassName("panel-customization-placeholder").length, "correct number of placeholders");
for (let i = 0; i < numPlaceholders; i++) {
// NB: We can't just iterate over all of the placeholders
// because each drag-drop action recreates them.
@ -393,7 +407,8 @@ add_task(function() {
"preferences-button",
"add-ons-button",
"edit-controls",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
simulateItemDrag(editControls, placeholder);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
@ -423,6 +438,8 @@ add_task(function() {
yield startCustomizing();
let editControls = document.getElementById("edit-controls");
let panel = document.getElementById(CustomizableUI.AREA_PANEL);
let numPlaceholders = isInWin8() ? 3 : 1;
is(panel.getElementsByClassName("panel-customization-placeholder").length, numPlaceholders, "correct number of placeholders");
let target = panel.getElementsByClassName("panel-customization-placeholder")[0];
let placementsAfterMove = ["zoom-controls",
"new-window-button",
@ -435,18 +452,22 @@ add_task(function() {
"preferences-button",
"add-ons-button",
"edit-controls",
"developer-button"];
"developer-button",
"social-share-button"];
addSwitchToMetroButtonInWindows8(placementsAfterMove);
if (isInWin8()) {
placementsAfterMove.splice(10, 1);
placementsAfterMove.push("edit-controls");
}
simulateItemDrag(editControls, target);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
let itemToDrag = "sync-button";
let button = document.getElementById(itemToDrag);
placementsAfterMove.splice(11, 0, itemToDrag);
if (isInWin8()) {
placementsAfterMove[10] = placementsAfterMove[11];
placementsAfterMove[11] = placementsAfterMove[12];
placementsAfterMove[12] = placementsAfterMove[13];
placementsAfterMove[13] = "edit-controls";
placementsAfterMove.push(itemToDrag);
} else {
placementsAfterMove.splice(10, 1, itemToDrag);
placementsAfterMove.push("edit-controls");
}
simulateItemDrag(button, editControls);
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);

View File

@ -11,15 +11,13 @@ add_task(function() {
yield startCustomizing();
let btn = document.getElementById("open-file-button");
let panel = document.getElementById(CustomizableUI.AREA_PANEL);
let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
CustomizableUI.removeWidgetFromArea("social-share-button");
if (isInWin8()) {
CustomizableUI.removeWidgetFromArea("switch-to-metro-button");
placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
} else {
ok(CustomizableUI.inDefaultState, "Should be in default state.");
}
let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
assertAreaPlacements(CustomizableUI.AREA_PANEL, placements);
is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders before exiting");
@ -28,6 +26,7 @@ add_task(function() {
yield startCustomizing();
is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders after re-entering");
CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_PANEL);
if (isInWin8()) {
CustomizableUI.addWidgetToArea("switch-to-metro-button", CustomizableUI.AREA_PANEL);
}
@ -39,6 +38,7 @@ add_task(function() {
yield startCustomizing();
let btn = document.getElementById("open-file-button");
let panel = document.getElementById(CustomizableUI.AREA_PANEL);
CustomizableUI.removeWidgetFromArea("social-share-button");
let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
let placementsAfterAppend = placements;
@ -49,7 +49,7 @@ add_task(function() {
}
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend);
is(CustomizableUI.inDefaultState, isInWin8(), "Should only be in default state if on Win8");
ok(!CustomizableUI.inDefaultState, "Should not be in default state");
is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder before exiting");
yield endCustomizing();
@ -63,26 +63,24 @@ add_task(function() {
btn = document.getElementById("open-file-button");
simulateItemDrag(btn, palette);
}
CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_PANEL);
ok(CustomizableUI.inDefaultState, "Should be in default state again.");
});
// Two orphaned items should have one placeholder next to them (case 2).
add_task(function() {
yield startCustomizing();
let btn = document.getElementById("add-ons-button");
let btn2 = document.getElementById("developer-button");
let btn3 = document.getElementById("switch-to-metro-button");
let buttonsToMove = ["add-ons-button", "developer-button", "social-share-button"];
if (isInWin8()) {
buttonsToMove.push("switch-to-metro-button");
}
let panel = document.getElementById(CustomizableUI.AREA_PANEL);
let palette = document.getElementById("customization-palette");
let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
let placementsAfterAppend = placements.filter(p => p != btn.id && p != btn2.id);
simulateItemDrag(btn, palette);
simulateItemDrag(btn2, palette);
if (isInWin8()) {
placementsAfterAppend = placementsAfterAppend.filter(p => p != btn3.id);
simulateItemDrag(btn3, palette);
let placementsAfterAppend = placements.filter(p => buttonsToMove.indexOf(p) < 0);
for (let i in buttonsToMove) {
CustomizableUI.removeWidgetFromArea(buttonsToMove[i]);
}
assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend);
@ -93,11 +91,8 @@ add_task(function() {
yield startCustomizing();
is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder after re-entering");
simulateItemDrag(btn, panel);
simulateItemDrag(btn2, panel);
if (isInWin8()) {
simulateItemDrag(btn3, panel);
for (let i in buttonsToMove) {
CustomizableUI.addWidgetToArea(buttonsToMove[i], CustomizableUI.AREA_PANEL);
}
assertAreaPlacements(CustomizableUI.AREA_PANEL, placements);
@ -112,6 +107,7 @@ add_task(function() {
let metroBtn = document.getElementById("switch-to-metro-button");
let panel = document.getElementById(CustomizableUI.AREA_PANEL);
let palette = document.getElementById("customization-palette");
CustomizableUI.removeWidgetFromArea("social-share-button");
let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
placements.pop();
@ -133,6 +129,7 @@ add_task(function() {
is(getVisiblePlaceholderCount(panel), 3, "Should have 3 visible placeholders after re-entering");
simulateItemDrag(developerButton, panel);
CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_PANEL);
if (isInWin8()) {
simulateItemDrag(metroBtn, panel);
}
@ -141,10 +138,10 @@ add_task(function() {
ok(CustomizableUI.inDefaultState, "Should be in default state again.");
});
// The default placements should have two placeholders at the bottom (or 1 in win8).
// The default placements should have one placeholder at the bottom (or 3 in metro-enabled win8).
add_task(function() {
yield startCustomizing();
let numPlaceholders = isInWin8() ? 1 : 2;
let numPlaceholders = isInWin8() ? 3 : 1;
let panel = document.getElementById(CustomizableUI.AREA_PANEL);
ok(CustomizableUI.inDefaultState, "Should be in default state.");
is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders before exiting");

View File

@ -36,6 +36,7 @@
<script type="text/javascript" src="loop/shared/js/router.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript" src="loop/js/desktopRouter.js"></script>
<script type="text/javascript" src="loop/js/conversation.js"></script>

View File

@ -189,21 +189,41 @@ loop.conversation = (function(OT, mozL10n) {
if (err) {
console.error("Failed to get the sessionData", err);
// XXX Not the ideal response, but bug 1047410 will be replacing
//this by better "call failed" UI.
// this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready");
return;
}
// XXX For incoming calls we might have more than one call queued.
// For now, we'll just assume the first call is the right information.
// We'll probably really want to be getting this data from the
// background worker on the desktop client.
// Bug 1032700 should fix this.
this._conversation.setIncomingSessionData(sessionData[0]);
this._setupWebSocketAndCallView();
});
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*/
_setupWebSocketAndCallView: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this._conversation.get("progressURL"),
websocketToken: this._conversation.get("websocketToken"),
callId: this._conversation.get("callId"),
});
this._websocket.promiseConnect().then(function() {
this.loadReactComponent(loop.conversation.IncomingCallView({
model: this._conversation,
video: {enabled: this._conversation.hasVideoStream("incoming")}
}));
});
}.bind(this), function() {
this._handleSessionError();
return;
}.bind(this));
},
/**
@ -214,13 +234,25 @@ loop.conversation = (function(OT, mozL10n) {
this._conversation.incoming();
},
/**
* Declines a call and handles closing of the window.
*/
_declineCall: function() {
this._websocket.decline();
// XXX Don't close the window straight away, but let any sends happen
// first. Ideally we'd wait to close the window until after we have a
// response from the server, to know that everything has completed
// successfully. However, that's quite difficult to ensure at the
// moment so we'll add it later.
setTimeout(window.close, 0);
},
/**
* Declines an incoming call.
*/
decline: function() {
navigator.mozLoop.stopAlerting();
// XXX For now, we just close the window
window.close();
this._declineCall();
},
/**
@ -238,7 +270,7 @@ loop.conversation = (function(OT, mozL10n) {
// (bug 1048909).
console.log(error);
});
window.close();
this._declineCall();
},
/**
@ -249,7 +281,7 @@ loop.conversation = (function(OT, mozL10n) {
if (!this._conversation.isSessionReady()) {
console.error("Error: navigated to conversation route without " +
"the start route to initialise the call first");
this._notifier.errorL10n("cannot_start_call_session_not_ready");
this._handleSessionError();
return;
}
@ -264,6 +296,15 @@ loop.conversation = (function(OT, mozL10n) {
}));
},
/**
* Handles a error starting the session
*/
_handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready");
},
/**
* Call has ended, display a feedback form.
*/

View File

@ -189,21 +189,41 @@ loop.conversation = (function(OT, mozL10n) {
if (err) {
console.error("Failed to get the sessionData", err);
// XXX Not the ideal response, but bug 1047410 will be replacing
//this by better "call failed" UI.
// this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready");
return;
}
// XXX For incoming calls we might have more than one call queued.
// For now, we'll just assume the first call is the right information.
// We'll probably really want to be getting this data from the
// background worker on the desktop client.
// Bug 1032700 should fix this.
this._conversation.setIncomingSessionData(sessionData[0]);
this._setupWebSocketAndCallView();
});
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*/
_setupWebSocketAndCallView: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this._conversation.get("progressURL"),
websocketToken: this._conversation.get("websocketToken"),
callId: this._conversation.get("callId"),
});
this._websocket.promiseConnect().then(function() {
this.loadReactComponent(loop.conversation.IncomingCallView({
model: this._conversation,
video: {enabled: this._conversation.hasVideoStream("incoming")}
}));
});
}.bind(this), function() {
this._handleSessionError();
return;
}.bind(this));
},
/**
@ -214,13 +234,25 @@ loop.conversation = (function(OT, mozL10n) {
this._conversation.incoming();
},
/**
* Declines a call and handles closing of the window.
*/
_declineCall: function() {
this._websocket.decline();
// XXX Don't close the window straight away, but let any sends happen
// first. Ideally we'd wait to close the window until after we have a
// response from the server, to know that everything has completed
// successfully. However, that's quite difficult to ensure at the
// moment so we'll add it later.
setTimeout(window.close, 0);
},
/**
* Declines an incoming call.
*/
decline: function() {
navigator.mozLoop.stopAlerting();
// XXX For now, we just close the window
window.close();
this._declineCall();
},
/**
@ -238,7 +270,7 @@ loop.conversation = (function(OT, mozL10n) {
// (bug 1048909).
console.log(error);
});
window.close();
this._declineCall();
},
/**
@ -249,7 +281,7 @@ loop.conversation = (function(OT, mozL10n) {
if (!this._conversation.isSessionReady()) {
console.error("Error: navigated to conversation route without " +
"the start route to initialise the call first");
this._notifier.errorL10n("cannot_start_call_session_not_ready");
this._handleSessionError();
return;
}
@ -264,6 +296,15 @@ loop.conversation = (function(OT, mozL10n) {
}));
},
/**
* Handles a error starting the session
*/
_handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready");
},
/**
* Call has ended, display a feedback form.
*/

View File

@ -25,6 +25,11 @@ loop.shared.models = (function() {
sessionId: undefined, // OT session id
sessionToken: undefined, // OT session token
apiKey: undefined, // OT api key
callId: undefined, // The callId on the server
progressURL: undefined, // The websocket url to use for progress
websocketToken: undefined, // The token to use for websocket auth, this is
// stored as a hex string which is what the server
// requires.
callType: undefined, // The type of incoming call selected by
// other peer ("audio" or "audio-video")
selectedCallType: undefined // The selected type for the call that was
@ -140,9 +145,12 @@ loop.shared.models = (function() {
setOutgoingSessionData: function(sessionData) {
// Explicit property assignment to prevent later "surprises"
this.set({
sessionId: sessionData.sessionId,
sessionToken: sessionData.sessionToken,
apiKey: sessionData.apiKey
sessionId: sessionData.sessionId,
sessionToken: sessionData.sessionToken,
apiKey: sessionData.apiKey,
callId: sessionData.callId,
progressURL: sessionData.progressURL,
websocketToken: sessionData.websocketToken.toString(16)
});
},
@ -154,10 +162,13 @@ loop.shared.models = (function() {
setIncomingSessionData: function(sessionData) {
// Explicit property assignment to prevent later "surprises"
this.set({
sessionId: sessionData.sessionId,
sessionToken: sessionData.sessionToken,
apiKey: sessionData.apiKey,
callType: sessionData.callType || "audio-video"
sessionId: sessionData.sessionId,
sessionToken: sessionData.sessionToken,
apiKey: sessionData.apiKey,
callId: sessionData.callId,
progressURL: sessionData.progressURL,
websocketToken: sessionData.websocketToken.toString(16),
callType: sessionData.callType || "audio-video"
});
},

View File

@ -0,0 +1,237 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true */
var loop = loop || {};
loop.CallConnectionWebSocket = (function() {
"use strict";
// Response timeout is 5 seconds as per API.
var kResponseTimeout = 5000;
/**
* Handles a websocket specifically for a call connection.
*
* There should be one of these created for each call connection.
*
* options items:
* - url The url of the websocket to connect to.
* - callId The call id for the call
* - websocketToken The authentication token for the websocket
*
* @param {Object} options The options for this websocket.
*/
function CallConnectionWebSocket(options) {
this.options = options || {};
if (!this.options.url) {
throw new Error("No url in options");
}
if (!this.options.callId) {
throw new Error("No callId in options");
}
if (!this.options.websocketToken) {
throw new Error("No websocketToken in options");
}
// Save the debug pref now, to avoid getting it each time.
if (navigator.mozLoop) {
this._debugWebSocket =
navigator.mozLoop.getLoopBoolPref("debug.websocket");
}
_.extend(this, Backbone.Events);
};
CallConnectionWebSocket.prototype = {
/**
* Start the connection to the websocket.
*
* @return {Promise} A promise that resolves when the websocket
* server connection is open and "hello"s have been
* exchanged. It is rejected if there is a failure in
* connection or the initial exchange of "hello"s.
*/
promiseConnect: function() {
var promise = new Promise(
function(resolve, reject) {
this.socket = new WebSocket(this.options.url);
this.socket.onopen = this._onopen.bind(this);
this.socket.onmessage = this._onmessage.bind(this);
this.socket.onerror = this._onerror.bind(this);
this.socket.onclose = this._onclose.bind(this);
var timeout = setTimeout(function() {
if (this.connectDetails && this.connectDetails.reject) {
this.connectDetails.reject("timeout");
this._clearConnectionFlags();
}
}.bind(this), kResponseTimeout);
this.connectDetails = {
resolve: resolve,
reject: reject,
timeout: timeout
};
}.bind(this));
return promise;
},
_clearConnectionFlags: function() {
clearTimeout(this.connectDetails.timeout);
delete this.connectDetails;
},
/**
* Internal function called to resolve the connection promise.
*
* It will log an error if no promise is found.
*/
_completeConnection: function() {
if (this.connectDetails && this.connectDetails.resolve) {
this.connectDetails.resolve();
this._clearConnectionFlags();
return;
}
console.error("Failed to complete connection promise - no promise available");
},
/**
* Checks if the websocket is connecting, and rejects the connection
* promise if appropriate.
*
* @param {Object} event The event to reject the promise with if
* appropriate.
*/
_checkConnectionFailed: function(event) {
if (this.connectDetails && this.connectDetails.reject) {
this.connectDetails.reject(event);
this._clearConnectionFlags();
return true;
}
return false;
},
/**
* Notifies the server that the user has declined the call.
*/
decline: function() {
this._send({
messageType: "action",
event: "terminate",
reason: "reject"
});
},
/**
* Sends data on the websocket.
*
* @param {Object} data The data to send.
*/
_send: function(data) {
this._log("WS Sending", data);
this.socket.send(JSON.stringify(data));
},
/**
* Used to determine if the server state is in a completed state, i.e.
* the server has determined the connection is terminated or connected.
*
* @return True if the last received state is terminated or connected.
*/
get _stateIsCompleted() {
return this._lastServerState === "terminated" ||
this._lastServerState === "connected";
},
/**
* Called when the socket is open. Automatically sends a "hello"
* message to the server.
*/
_onopen: function() {
// Auto-register with the server.
this._send({
messageType: "hello",
callId: this.options.callId,
auth: this.options.websocketToken
});
},
/**
* Called when a message is received from the server.
*
* @param {Object} event The websocket onmessage event.
*/
_onmessage: function(event) {
var msg;
try {
msg = JSON.parse(event.data);
} catch (x) {
console.error("Error parsing received message:", x);
return;
}
this._log("WS Receiving", event.data);
this._lastServerState = msg.state;
switch(msg.messageType) {
case "hello":
this._completeConnection();
break;
case "progress":
this.trigger("progress", msg);
break;
}
},
/**
* Called when there is an error on the websocket.
*
* @param {Object} event A simple error event.
*/
_onerror: function(event) {
this._log("WS Error", event);
if (!this._stateIsCompleted &&
!this._checkConnectionFailed(event)) {
this.trigger("error", event);
}
},
/**
* Called when the websocket is closed.
*
* @param {CloseEvent} event The details of the websocket closing.
*/
_onclose: function(event) {
this._log("WS Close", event);
// If the websocket goes away when we're not in a completed state
// then its an error. So we either pass it back via the connection
// promise, or trigger the closed event.
if (!this._stateIsCompleted &&
!this._checkConnectionFailed(event)) {
this.trigger("closed", event);
}
},
/**
* Logs debug to the console.
*
* Parameters: same as console.log
*/
_log: function() {
if (this._debugWebSocket) {
console.log.apply(console, arguments);
}
}
};
return CallConnectionWebSocket;
})();

View File

@ -46,6 +46,7 @@ browser.jar:
content/browser/loop/shared/js/router.js (content/shared/js/router.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
# Shared libs
content/browser/loop/shared/libs/react-0.11.1.js (content/shared/libs/react-0.11.1.js)

View File

@ -37,6 +37,7 @@
<script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/router.js"></script>
<script type="text/javascript" src="shared/js/websocket.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/webapp.js"></script>

View File

@ -374,12 +374,67 @@ loop.webapp = (function($, _, OT, webL10n) {
this._notifier.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
} else {
this._setupWebSocketAndCallView(loopToken);
}
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*
* @param {string} loopToken The session token to use.
*/
_setupWebSocketAndCallView: function(loopToken) {
this._websocket = new loop.CallConnectionWebSocket({
url: this._conversation.get("progressURL"),
websocketToken: this._conversation.get("websocketToken"),
callId: this._conversation.get("callId"),
});
this._websocket.promiseConnect().then(function() {
this.navigate("call/ongoing/" + loopToken, {
trigger: true
});
}.bind(this), function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready");
return;
}.bind(this));
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Used to receive websocket progress and to determine how to handle
* it if appropraite.
*/
_handleWebSocketProgress: function(progressData) {
if (progressData.state === "terminated") {
// XXX Before adding more states here, the basic protocol messages to the
// server need implementing on both the standalone and desktop side.
// These are covered by bug 1045643, but also check the dependencies on
// bug 1034041.
//
// Failure to do this will break desktop - standalone call setup. We're
// ok to handle reject, as that is a specific message from the destkop via
// the server.
switch (progressData.reason) {
case "reject":
this._handleCallRejected();
}
}
},
/**
* Handles call rejection.
* XXX This should really display the call failed view - bug 1046959
* will implement this.
*/
_handleCallRejected: function() {
this.endCall();
this._notifier.errorL10n("call_timeout_notification_text");
},
/**
* @override {loop.shared.router.BaseConversationRouter.endCall}
*/

View File

@ -374,12 +374,67 @@ loop.webapp = (function($, _, OT, webL10n) {
this._notifier.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
} else {
this._setupWebSocketAndCallView(loopToken);
}
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*
* @param {string} loopToken The session token to use.
*/
_setupWebSocketAndCallView: function(loopToken) {
this._websocket = new loop.CallConnectionWebSocket({
url: this._conversation.get("progressURL"),
websocketToken: this._conversation.get("websocketToken"),
callId: this._conversation.get("callId"),
});
this._websocket.promiseConnect().then(function() {
this.navigate("call/ongoing/" + loopToken, {
trigger: true
});
}.bind(this), function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this._notifier.errorL10n("cannot_start_call_session_not_ready");
return;
}.bind(this));
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Used to receive websocket progress and to determine how to handle
* it if appropraite.
*/
_handleWebSocketProgress: function(progressData) {
if (progressData.state === "terminated") {
// XXX Before adding more states here, the basic protocol messages to the
// server need implementing on both the standalone and desktop side.
// These are covered by bug 1045643, but also check the dependencies on
// bug 1034041.
//
// Failure to do this will break desktop - standalone call setup. We're
// ok to handle reject, as that is a specific message from the destkop via
// the server.
switch (progressData.reason) {
case "reject":
this._handleCallRejected();
}
}
},
/**
* Handles call rejection.
* XXX This should really display the call failed view - bug 1046959
* will implement this.
*/
_handleCallRejected: function() {
this.endCall();
this._notifier.errorL10n("call_timeout_notification_text");
},
/**
* @override {loop.shared.router.BaseConversationRouter.endCall}
*/

View File

@ -15,6 +15,7 @@ describe("loop.conversation", function() {
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
notifier = {
notify: sandbox.spy(),
warn: sandbox.spy(),
@ -119,7 +120,6 @@ describe("loop.conversation", function() {
pendingCallTimeout: 1000,
});
sandbox.stub(client, "requestCallsInfo");
sandbox.stub(conversation, "setIncomingSessionData");
sandbox.stub(conversation, "setOutgoingSessionData");
});
@ -187,53 +187,125 @@ describe("loop.conversation", function() {
});
describe("requestCallsInfo successful", function() {
var fakeSessionData;
var fakeSessionData, resolvePromise, rejectPromise;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType"
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: 123
};
sandbox.stub(router, "_setupWebSocketAndCallView");
sandbox.stub(conversation, "setIncomingSessionData");
client.requestCallsInfo.callsArgWith(1, null, [fakeSessionData]);
});
it("should store the session data", function() {
router.incoming(42);
router.incoming("fakeVersion");
sinon.assert.calledOnce(conversation.setIncomingSessionData);
sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
fakeSessionData);
});
it("should call the view with video.enabled=false", function() {
sandbox.stub(conversation, "get").withArgs("callType").returns("audio");
it("should call #_setupWebSocketAndCallView", function() {
router.incoming("fakeVersion");
sinon.assert.calledOnce(conversation.get);
sinon.assert.calledOnce(loop.conversation.IncomingCallView);
sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
{model: conversation,
video: {enabled: false}});
sinon.assert.calledOnce(router._setupWebSocketAndCallView);
sinon.assert.calledWithExactly(router._setupWebSocketAndCallView);
});
});
describe("#_setupWebSocketAndCallView", function() {
beforeEach(function() {
conversation.setIncomingSessionData({
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: 123
});
});
it("should display the incoming call view", function() {
sandbox.stub(conversation, "get").withArgs("callType")
.returns("audio-video");
router.incoming("fakeVersion");
describe("Websocket connection successful", function() {
var promise;
sinon.assert.calledOnce(loop.conversation.IncomingCallView);
sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
{model: conversation,
video: {enabled: true}});
sinon.assert.calledOnce(router.loadReactComponent);
sinon.assert.calledWith(router.loadReactComponent,
sinon.match(function(value) {
return TestUtils.isDescriptorOfType(value,
loop.conversation.IncomingCallView);
}));
beforeEach(function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() {
promise = new Promise(function(resolve, reject) {
resolve();
});
return promise;
}
});
});
it("should create a CallConnectionWebSocket", function(done) {
router._setupWebSocketAndCallView();
promise.then(function () {
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
callId: "Hello",
url: "http://progress.example.com",
// The websocket token is converted to a hex string.
websocketToken: "7b"
});
done();
});
});
it("should create the view with video.enabled=false", function(done) {
sandbox.stub(conversation, "get").withArgs("callType").returns("audio");
router._setupWebSocketAndCallView();
promise.then(function () {
sinon.assert.called(conversation.get);
sinon.assert.calledOnce(loop.conversation.IncomingCallView);
sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
{model: conversation,
video: {enabled: false}});
done();
});
});
});
describe("Websocket connection failed", function() {
var promise;
beforeEach(function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() {
promise = new Promise(function(resolve, reject) {
reject();
});
return promise;
}
});
});
it("should display an error", function(done) {
router._setupWebSocketAndCallView();
promise.then(function() {
}, function () {
sinon.assert.calledOnce(router._notifier.errorL10n);
sinon.assert.calledWithExactly(router._notifier.errorL10n,
"cannot_start_call_session_not_ready");
done();
});
});
});
});
});
@ -291,10 +363,14 @@ describe("loop.conversation", function() {
describe("#decline", function() {
beforeEach(function() {
sandbox.stub(window, "close");
router._websocket = {
decline: sandbox.spy()
};
});
it("should close the window", function() {
router.decline();
sandbox.clock.tick(1);
sinon.assert.calledOnce(window.close);
});
@ -345,6 +421,13 @@ describe("loop.conversation", function() {
});
describe("#blocked", function() {
beforeEach(function() {
router._websocket = {
decline: sandbox.spy()
};
sandbox.stub(window, "close");
});
it("should call mozLoop.stopAlerting", function() {
sandbox.stub(navigator.mozLoop, "stopAlerting");
router.declineAndBlock();
@ -375,9 +458,10 @@ describe("loop.conversation", function() {
});
it("should close the window", function() {
sandbox.stub(window, "close");
router.declineAndBlock();
sandbox.clock.tick(1);
sinon.assert.calledOnce(window.close);
});
});

View File

@ -37,6 +37,7 @@
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/router.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/js/client.js"></script>
<script src="../../content/js/desktopRouter.js"></script>
<script src="../../content/js/conversation.js"></script>

View File

@ -1,7 +1,9 @@
[DEFAULT]
support-files =
head.js
loop_fxa.sjs
[browser_loop_fxa_server.js]
[browser_mozLoop_appVersionInfo.js]
[browser_mozLoop_prefs.js]
[browser_mozLoop_doNotDisturb.js]

View File

@ -0,0 +1,127 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test the server mocking FxA integration endpoints on the Loop server.
*/
"use strict";
const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?";
registerCleanupFunction(function* () {
yield promiseDeletedOAuthParams(BASE_URL);
});
add_task(function* required_setup_params() {
let params = {
client_id: "my_client_id",
content_uri: "https://example.com/content/",
oauth_uri: "https://example.com/oauth/",
profile_uri: "https://example.com/profile/",
state: "my_state",
};
let request = yield promiseOAuthParamsSetup(BASE_URL, params);
is(request.status, 200, "Check /setup_params status");
request = yield promiseParams();
is(request.status, 200, "Check /fxa-oauth/params status");
for (let param of Object.keys(params)) {
is(request.response[param], params[param], "Check /fxa-oauth/params " + param);
}
});
add_task(function* optional_setup_params() {
let params = {
action: "signin",
client_id: "my_client_id",
content_uri: "https://example.com/content/",
oauth_uri: "https://example.com/oauth/",
profile_uri: "https://example.com/profile/",
scope: "profile",
state: "my_state",
};
let request = yield promiseOAuthParamsSetup(BASE_URL, params);
is(request.status, 200, "Check /setup_params status");
request = yield promiseParams();
is(request.status, 200, "Check /fxa-oauth/params status");
for (let param of Object.keys(params)) {
is(request.response[param], params[param], "Check /fxa-oauth/params " + param);
}
});
add_task(function* delete_setup_params() {
yield promiseDeletedOAuthParams(BASE_URL);
let request = yield promiseParams();
is(Object.keys(request.response).length, 0, "Params should have been deleted");
});
// Begin /fxa-oauth/token tests
add_task(function* token_request() {
let params = {
client_id: "my_client_id",
content_uri: "https://example.com/content/",
oauth_uri: "https://example.com/oauth/",
profile_uri: "https://example.com/profile/",
state: "my_state",
};
yield promiseOAuthParamsSetup(BASE_URL, params);
let request = yield promiseToken("my_code", params.state);
ise(request.status, 200, "Check token response status");
ise(request.response.access_token, "my_code_access_token", "Check access_token");
ise(request.response.scopes, "", "Check scopes");
ise(request.response.token_type, "bearer", "Check token_type");
});
add_task(function* token_request_invalid_state() {
let params = {
client_id: "my_client_id",
content_uri: "https://example.com/content/",
oauth_uri: "https://example.com/oauth/",
profile_uri: "https://example.com/profile/",
state: "my_invalid_state",
};
yield promiseOAuthParamsSetup(BASE_URL, params);
let request = yield promiseToken("my_code", "my_state");
ise(request.status, 400, "Check token response status");
ise(request.response, null, "Check token response body");
});
// Helper methods
function promiseParams() {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", BASE_URL + "/fxa-oauth/params", true);
xhr.responseType = "json";
xhr.addEventListener("load", () => {
info("/fxa-oauth/params response:\n" + JSON.stringify(xhr.response, null, 4));
deferred.resolve(xhr);
});
xhr.addEventListener("error", deferred.reject);
xhr.send();
return deferred.promise;
}
function promiseToken(code, state) {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", BASE_URL + "/fxa-oauth/token", true);
xhr.responseType = "json";
xhr.addEventListener("load", () => {
info("/fxa-oauth/token response:\n" + JSON.stringify(xhr.response, null, 4));
deferred.resolve(xhr);
});
xhr.addEventListener("error", deferred.reject);
let payload = {
code: code,
state: state,
};
xhr.send(JSON.stringify(payload, null, 4));
return deferred.promise;
}

View File

@ -69,3 +69,28 @@ function loadLoopPanel() {
// Now get the actual API.
yield promiseGetMozLoopAPI();
}
function promiseOAuthParamsSetup(baseURL, params) {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", baseURL + "/setup_params", true);
xhr.setRequestHeader("X-Params", JSON.stringify(params));
xhr.addEventListener("load", () => deferred.resolve(xhr));
xhr.addEventListener("error", error => deferred.reject(error));
xhr.send();
return deferred.promise;
}
function promiseDeletedOAuthParams(baseURL) {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("DELETE", baseURL + "/setup_params", true);
xhr.addEventListener("load", () => deferred.resolve(xhr));
xhr.addEventListener("error", error => deferred.reject(error));
xhr.send();
return deferred.promise;
}

View File

@ -0,0 +1,118 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* This is a mock server that implements the FxA endpoints on the Loop server.
*/
"use strict";
const REQUIRED_PARAMS = ["client_id", "content_uri", "oauth_uri", "profile_uri", "state"];
Components.utils.import("resource://gre/modules/NetUtil.jsm");
/**
* Entry point for HTTP requests.
*/
function handleRequest(request, response) {
switch (request.queryString) {
case "/setup_params":
setup_params(request, response);
return;
case "/fxa-oauth/params":
params(request, response);
return;
case "/fxa-oauth/token":
token(request, response);
return;
}
response.setStatusLine(request.httpVersion, 404, "Not Found");
}
/**
* POST /setup_params
* DELETE /setup_params
*
* Test-only endpoint to setup the /fxa-oauth/params response.
*
* For a POST the X-Params header should contain a JSON object with keys to set for /fxa-oauth/params.
* A DELETE request will delete the stored parameters and should be run in a cleanup function to
* avoid interfering with subsequen tests.
*/
function setup_params(request, response) {
response.setHeader("Content-Type", "text/plain", false);
if (request.method == "DELETE") {
setSharedState("/fxa-oauth/params", "");
response.write("Params deleted");
return;
}
let params = JSON.parse(request.getHeader("X-Params"));
if (!params) {
response.setStatusLine(request.httpVersion, 400, "Bad Request");
return;
}
setSharedState("/fxa-oauth/params", JSON.stringify(params));
response.write("Params updated");
}
/**
* POST /fxa-oauth/params endpoint
*
* Fetch OAuth parameters used to start the OAuth flow in the browser.
* Parameters: None
* Response: JSON containing an object of oauth parameters.
*/
function params(request, response) {
if (request.method != "POST") {
response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
response.setHeader("Allow", "POST", false);
// Add a button to make a POST request to make this endpoint easier to debug in the browser.
response.write("<form method=POST><button type=submit>POST</button></form>");
return;
}
let origin = request.scheme + "://" + request.host + ":" + request.port;
let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}");
// Warn if required parameters are missing.
for (let paramName of REQUIRED_PARAMS) {
if (!(paramName in params)) {
dump("Warning: " + paramName + " is a required parameter\n");
}
}
// Save the result so we have the effective `state` value.
setSharedState("/fxa-oauth/params", JSON.stringify(params));
response.setHeader("Content-Type", "application/json; charset=utf-8", false);
response.write(JSON.stringify(params, null, 2));
}
/**
* POST /fxa-oauth/token
*
* Validate the state parameter with the server session state and if it matches, exchange the code
* for an OAuth Token.
* Parameters: code & state as JSON in the POST body.
* Response: JSON containing an object of OAuth token information.
*/
function token(request, response) {
let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}");
let body = NetUtil.readInputStreamToString(request.bodyInputStream,
request.bodyInputStream.available());
let payload = JSON.parse(body);
if (!params.state || params.state !== payload.state) {
response.setStatusLine(request.httpVersion, 400, "State mismatch");
response.write("State mismatch");
return;
}
let tokenData = {
access_token: payload.code + "_access_token",
scopes: "",
token_type: "bearer",
};
response.setHeader("Content-Type", "application/json; charset=utf-8", false);
response.write(JSON.stringify(tokenData, null, 2));
}

View File

@ -36,12 +36,14 @@
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/router.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>
<script src="views_test.js"></script>
<script src="router_test.js"></script>
<script src="websocket_test.js"></script>
<script src="feedbackApiClient_test.js"></script>
<script>
mocha.run(function () {

View File

@ -22,10 +22,11 @@ describe("loop.shared.models", function() {
requests.push(xhr);
};
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType"
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType",
websocketToken: 123
};
fakeSession = _.extend({
connect: function () {},

View File

@ -0,0 +1,224 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/*global loop, sinon, it, beforeEach, afterEach, describe */
var expect = chai.expect;
describe("loop.CallConnectionWebSocket", function() {
"use strict";
var sandbox,
dummySocket;
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
dummySocket = {
send: sinon.spy()
};
sandbox.stub(window, 'WebSocket').returns(dummySocket);
});
afterEach(function() {
sandbox.restore();
});
describe("#constructor", function() {
it("should require a url option", function() {
expect(function() {
return new loop.CallConnectionWebSocket();
}).to.Throw(/No url/);
});
it("should require a callId setting", function() {
expect(function() {
return new loop.CallConnectionWebSocket({url: "wss://fake/"});
}).to.Throw(/No callId/);
});
it("should require a websocketToken setting", function() {
expect(function() {
return new loop.CallConnectionWebSocket({
url: "http://fake/",
callId: "hello"
});
}).to.Throw(/No websocketToken/);
});
});
describe("constructed", function() {
var callWebSocket, fakeUrl, fakeCallId, fakeWebSocketToken;
beforeEach(function() {
fakeUrl = "wss://fake/";
fakeCallId = "callId";
fakeWebSocketToken = "7b";
callWebSocket = new loop.CallConnectionWebSocket({
url: fakeUrl,
callId: fakeCallId,
websocketToken: fakeWebSocketToken
});
});
describe("#promiseConnect", function() {
it("should create a new websocket connection", function() {
callWebSocket.promiseConnect();
sinon.assert.calledOnce(window.WebSocket);
sinon.assert.calledWithExactly(window.WebSocket, fakeUrl);
});
it("should reject the promise if connection is not completed in " +
"5 seconds", function(done) {
var promise = callWebSocket.promiseConnect();
sandbox.clock.tick(5101);
promise.then(function() {}, function(error) {
expect(error).to.be.equal("timeout");
done();
});
});
it("should reject the promise if the connection errors", function(done) {
var promise = callWebSocket.promiseConnect();
dummySocket.onerror("error");
promise.then(function() {}, function(error) {
expect(error).to.be.equal("error");
done();
});
});
it("should reject the promise if the connection closes", function(done) {
var promise = callWebSocket.promiseConnect();
dummySocket.onclose("close");
promise.then(function() {}, function(error) {
expect(error).to.be.equal("close");
done();
});
});
it("should send hello when the socket is opened", function() {
callWebSocket.promiseConnect();
dummySocket.onopen();
sinon.assert.calledOnce(dummySocket.send);
sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
messageType: "hello",
callId: fakeCallId,
auth: fakeWebSocketToken
}));
});
it("should resolve the promise when the 'hello' is received",
function(done) {
var promise = callWebSocket.promiseConnect();
dummySocket.onmessage({
data: '{"messageType":"hello", "state":"init"}'
});
promise.then(function() {
done();
});
});
});
describe("#decline", function() {
it("should send a terminate message to the server", function() {
callWebSocket.promiseConnect();
callWebSocket.decline();
sinon.assert.calledOnce(dummySocket.send);
sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
messageType: "action",
event: "terminate",
reason: "reject"
}));
});
});
describe("Events", function() {
beforeEach(function() {
sandbox.stub(callWebSocket, "trigger");
callWebSocket.promiseConnect();
});
describe("Progress", function() {
it("should trigger a progress event on the callWebSocket", function() {
var eventData = {
messageType: "progress",
state: "terminate",
reason: "reject"
};
dummySocket.onmessage({
data: JSON.stringify(eventData)
});
sinon.assert.calledOnce(callWebSocket.trigger);
sinon.assert.calledWithExactly(callWebSocket.trigger, "progress", eventData);
});
});
describe("Error", function() {
// Handled in constructed -> #promiseConnect:
// should reject the promise if the connection errors
it("should trigger an error if state is not completed", function() {
callWebSocket._clearConnectionFlags();
dummySocket.onerror("Error");
sinon.assert.calledOnce(callWebSocket.trigger);
sinon.assert.calledWithExactly(callWebSocket.trigger,
"error", "Error");
});
it("should not trigger an error if state is completed", function() {
callWebSocket._clearConnectionFlags();
callWebSocket._lastServerState = "connected";
dummySocket.onerror("Error");
sinon.assert.notCalled(callWebSocket.trigger);
});
});
describe("Close", function() {
// Handled in constructed -> #promiseConnect:
// should reject the promise if the connection closes
it("should trigger a close event if state is not completed", function() {
callWebSocket._clearConnectionFlags();
dummySocket.onclose("Error");
sinon.assert.calledOnce(callWebSocket.trigger);
sinon.assert.calledWithExactly(callWebSocket.trigger,
"closed", "Error");
});
it("should not trigger an error if state is completed", function() {
callWebSocket._clearConnectionFlags();
callWebSocket._lastServerState = "terminated";
dummySocket.onclose("Error");
sinon.assert.notCalled(callWebSocket.trigger);
});
});
});
});
});

View File

@ -35,6 +35,7 @@
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/router.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../standalone/content/js/standaloneClient.js"></script>
<script src="../../standalone/content/js/webapp.js"></script>
<!-- Test scripts -->

View File

@ -95,6 +95,10 @@ describe("loop.webapp", function() {
});
describe("#startCall", function() {
beforeEach(function() {
sandbox.stub(router, "_setupWebSocketAndCallView");
});
it("should navigate back home if session token is missing", function() {
router.startCall();
@ -110,15 +114,146 @@ describe("loop.webapp", function() {
"missing_conversation_info");
});
it("should navigate to call/ongoing/:token if session token is available",
function() {
conversation.set("loopToken", "fake");
it("should setup the websocket if session token is available", function() {
conversation.set("loopToken", "fake");
router.startCall();
router.startCall();
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
sinon.assert.calledOnce(router._setupWebSocketAndCallView);
sinon.assert.calledWithExactly(router._setupWebSocketAndCallView, "fake");
});
});
describe("#_setupWebSocketAndCallView", function() {
beforeEach(function() {
conversation.setOutgoingSessionData({
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: 123
});
});
describe("Websocket connection successful", function() {
var promise;
beforeEach(function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() {
promise = new Promise(function(resolve, reject) {
resolve();
});
return promise;
},
on: sandbox.spy()
});
});
it("should create a CallConnectionWebSocket", function(done) {
router._setupWebSocketAndCallView("fake");
promise.then(function () {
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
callId: "Hello",
url: "http://progress.example.com",
// The websocket token is converted to a hex string.
websocketToken: "7b"
});
done();
});
});
it("should navigate to call/ongoing/:token", function(done) {
router._setupWebSocketAndCallView("fake");
promise.then(function () {
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
done();
});
});
});
describe("Websocket connection failed", function() {
var promise;
beforeEach(function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() {
promise = new Promise(function(resolve, reject) {
reject();
});
return promise;
},
on: sandbox.spy()
});
});
it("should display an error", function() {
router._setupWebSocketAndCallView();
promise.then(function() {
}, function () {
sinon.assert.calledOnce(router._notifier.errorL10n);
sinon.assert.calledWithExactly(router._notifier.errorL10n,
"cannot_start_call_session_not_ready");
done();
});
});
});
describe("Websocket Events", function() {
beforeEach(function() {
conversation.setOutgoingSessionData({
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: 123
});
sandbox.stub(loop.CallConnectionWebSocket.prototype,
"promiseConnect").returns({
then: sandbox.spy()
});
router._setupWebSocketAndCallView();
});
describe("Progress", function() {
describe("state: terminate, reason: reject", function() {
beforeEach(function() {
sandbox.stub(router, "endCall");
});
it("should end the call", function() {
router._websocket.trigger("progress", {
state: "terminated",
reason: "reject"
});
sinon.assert.calledOnce(router.endCall);
});
it("should display an error message", function() {
router._websocket.trigger("progress", {
state: "terminated",
reason: "reject"
});
sinon.assert.calledOnce(router._notifier.errorL10n);
sinon.assert.calledWithExactly(router._notifier.errorL10n,
"call_timeout_notification_text");
});
});
});
});
});
describe("#endCall", function() {
@ -241,20 +376,21 @@ describe("loop.webapp", function() {
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
websocketToken: 123
};
conversation.set("loopToken", "fakeToken");
sandbox.stub(router, "startCall");
});
it("should navigate to call/ongoing/:token once call session is ready",
it("should attempt to start the call once call session is ready",
function() {
router.setupOutgoingCall();
conversation.outgoing(fakeSessionData);
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWith(router.navigate, "call/ongoing/fakeToken");
sinon.assert.calledOnce(router.startCall);
});
it("should navigate to call/{token} when conversation ended", function() {

View File

@ -29,7 +29,6 @@ function test() {
}
function testBasic(win, doc, policy) {
is(policy.dataSubmissionPolicyAccepted, false, "Data submission policy not accepted.");
is(policy.healthReportUploadEnabled, true, "Health Report upload enabled on app first run.");
let checkbox = doc.getElementById("submitHealthReportBox");

View File

@ -13,6 +13,8 @@ function runPaneTest(fn) {
.policy;
ok(policy, "Policy object defined");
resetPreferences();
fn(win, policy);
}
@ -21,11 +23,30 @@ function runPaneTest(fn) {
"chrome,titlebar,toolbar,centerscreen,dialog=no", "paneAdvanced");
}
let logDetails = {
dumpAppender: null,
rootLogger: null,
};
function test() {
waitForExplicitFinish();
resetPreferences();
registerCleanupFunction(resetPreferences);
let ld = logDetails;
registerCleanupFunction(() => {
ld.rootLogger.removeAppender(ld.dumpAppender);
delete ld.dumpAppender;
delete ld.rootLogger;
});
let ns = {};
Cu.import("resource://gre/modules/Log.jsm", ns);
ld.rootLogger = ns.Log.repository.rootLogger;
ld.dumpAppender = new ns.Log.DumpAppender();
ld.dumpAppender.level = ns.Log.Level.All;
ld.rootLogger.addAppender(ld.dumpAppender);
Services.prefs.lockPref("datareporting.healthreport.uploadEnabled");
runPaneTest(testUploadDisabled);
}
@ -43,7 +64,8 @@ function testUploadDisabled(win, policy) {
function testBasic(win, policy) {
let doc = win.document;
is(policy.dataSubmissionPolicyAccepted, false, "Data submission policy not accepted.");
resetPreferences();
is(policy.healthReportUploadEnabled, true, "Health Report upload enabled on app first run.");
let checkbox = doc.getElementById("submitHealthReportBox");
@ -63,6 +85,10 @@ function testBasic(win, policy) {
}
function resetPreferences() {
Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled");
let service = Cc["@mozilla.org/datareporting/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
service.policy._prefs.resetBranch("datareporting.policy.");
service.policy.dataSubmissionPolicyBypassNotification = true;
}

View File

@ -6,6 +6,7 @@
function test() {
requestLongerTimeout(2);
waitForExplicitFinish();
resetPreferences();
try {
let cm = Components.classes["@mozilla.org/categorymanager;1"]
@ -101,3 +102,10 @@ function test() {
}
function resetPreferences() {
let service = Components.classes["@mozilla.org/datareporting/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
service.policy._prefs.resetBranch("datareporting.policy.");
service.policy.dataSubmissionPolicyBypassNotification = true;
}

View File

@ -1234,11 +1234,7 @@ SourceScripts.prototype = {
_onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) {
const item = DebuggerView.Sources.getItemByValue(url);
if (item) {
if (isBlackBoxed) {
item.prebuiltNode.classList.add("black-boxed");
} else {
item.prebuiltNode.classList.remove("black-boxed");
}
item.prebuiltNode.classList.toggle("black-boxed", isBlackBoxed);
}
DebuggerView.Sources.updateToolbarButtonsState();
DebuggerView.maybeShowBlackBoxMessage();
@ -1555,11 +1551,12 @@ Tracer.prototype = {
/**
* Callback for handling a new call frame.
*/
_onCall: function({ name, location, parameterNames, depth, arguments: args }) {
_onCall: function({ name, location, blackBoxed, parameterNames, depth, arguments: args }) {
const item = {
name: name,
location: location,
id: this._idCounter++
id: this._idCounter++,
blackBoxed
};
this._stack.push(item);
@ -1570,7 +1567,8 @@ Tracer.prototype = {
depth: depth,
parameterNames: parameterNames,
arguments: args,
frameId: item.id
frameId: item.id,
blackBoxed
});
},
@ -1582,14 +1580,15 @@ Tracer.prototype = {
return;
}
const { name, id, location } = this._stack.pop();
const { name, id, location, blackBoxed } = this._stack.pop();
DebuggerView.Tracer.addTrace({
type: aPacket.why,
name: name,
location: location,
depth: aPacket.depth,
frameId: id,
returnVal: aPacket.return || aPacket.throw || aPacket.yield
returnVal: aPacket.return || aPacket.throw || aPacket.yield,
blackBoxed
});
},

View File

@ -1437,10 +1437,11 @@ TracerView.prototype = Heritage.extend(WidgetMethods, {
* The network request view.
*/
_createView: function(aTrace) {
let { type, name, location, depth, frameId } = aTrace;
let { type, name, location, blackBoxed, depth, frameId } = aTrace;
let { parameterNames, returnVal, arguments: args } = aTrace;
let fragment = document.createDocumentFragment();
this._templateItem.classList.toggle("black-boxed", blackBoxed);
this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url));
this._templateItem.style.MozPaddingStart = depth + "em";

View File

@ -262,6 +262,7 @@ skip-if = os == "linux" || e10s # Bug 888811 & bug 891176
[browser_dbg_tracing-04.js]
[browser_dbg_tracing-05.js]
[browser_dbg_tracing-06.js]
[browser_dbg_tracing-07.js]
[browser_dbg_variables-view-01.js]
[browser_dbg_variables-view-02.js]
[browser_dbg_variables-view-03.js]

View File

@ -0,0 +1,86 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Execute code both before and after blackboxing and test that we get
* appropriately styled traces.
*/
const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
let gTab, gDebuggee, gPanel;
function test() {
Task.async(function*() {
yield pushPref();
[gTab, gDebuggee, gPanel] = yield initDebugger(TAB_URL);
yield startTracing(gPanel);
yield clickButton();
yield waitForClientEvents(gPanel, "traces");
/**
* Test that there are some traces which are not blackboxed.
*/
const firstBbButton = getBlackBoxButton(gPanel);
ok(!firstBbButton.checked, "Should not be black boxed by default");
const blackBoxedTraces =
gPanel.panelWin.document.querySelectorAll(".trace-item.black-boxed");
ok(blackBoxedTraces.length === 0, "There should no blackboxed traces.");
const notBlackBoxedTraces =
gPanel.panelWin.document.querySelectorAll(".trace-item:not(.black-boxed)");
ok(notBlackBoxedTraces.length > 0,
"There should be some traces which are not blackboxed.");
yield toggleBlackBoxing(gPanel);
yield clickButton();
yield waitForClientEvents(gPanel, "traces");
/**
* Test that there are some traces which are blackboxed.
*/
const secondBbButton = getBlackBoxButton(gPanel);
ok(secondBbButton.checked, "The checkbox should no longer be checked.");
const traces =
gPanel.panelWin.document.querySelectorAll(".trace-item.black-boxed");
ok(traces.length > 0, "There should be some blackboxed traces.");
yield stopTracing(gPanel);
yield popPref();
yield closeDebuggerAndFinish(gPanel);
finish();
})().catch(e => {
ok(false, "Got an error: " + e.message + "\n" + e.stack);
finish();
});
}
function clickButton() {
EventUtils.sendMouseEvent({ type: "click" },
gDebuggee.document.querySelector("button"),
gDebuggee);
}
function pushPref() {
let deferred = promise.defer();
SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]},
deferred.resolve);
return deferred.promise;
}
function popPref() {
let deferred = promise.defer();
SpecialPowers.popPrefEnv(deferred.resolve);
return deferred.promise;
}
registerCleanupFunction(function() {
gTab = null;
gDebuggee = null;
gPanel = null;
});

View File

@ -140,17 +140,18 @@ These should match what Safari and other Apple applications use on OS X Lion. --
<!ENTITY editThisBookmarkCmd.label "Edit This Bookmark">
<!ENTITY bookmarkThisPageCmd.commandkey "d">
<!ENTITY markPageCmd.commandkey "l">
<!ENTITY findShareServices.label "Find more Share services…">
<!ENTITY sharePageCmd.label "Share This Page">
<!ENTITY sharePageCmd.commandkey "S">
<!ENTITY sharePageCmd.accesskey "s">
<!ENTITY shareLinkCmd.label "Share This Link">
<!ENTITY shareLinkCmd.accesskey "s">
<!ENTITY shareLinkCmd.accesskey "h">
<!ENTITY shareImageCmd.label "Share This Image">
<!ENTITY shareImageCmd.accesskey "s">
<!ENTITY shareImageCmd.accesskey "r">
<!ENTITY shareSelectCmd.label "Share Selection">
<!ENTITY shareSelectCmd.accesskey "s">
<!ENTITY shareSelectCmd.accesskey "r">
<!ENTITY shareVideoCmd.label "Share This Video">
<!ENTITY shareVideoCmd.accesskey "s">
<!ENTITY shareVideoCmd.accesskey "r">
<!ENTITY feedsMenu.label "Subscribe">
<!ENTITY subscribeToPageMenupopup.label "Subscribe to This Page">
<!ENTITY subscribeToPageMenuitem.label "Subscribe to This Page…">

View File

@ -363,7 +363,8 @@ SocialErrorListener.prototype = {
if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin"));
provider.errorState = "content-error";
if (provider && !provider.errorState)
provider.errorState = "content-error";
this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler);
}
@ -373,7 +374,7 @@ SocialErrorListener.prototype = {
if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin"));
if (!provider.errorState)
if (provider && !provider.errorState)
provider.errorState = "content-error";
schedule(function() {
this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)

View File

@ -1313,6 +1313,11 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-ic
width: 16px;
}
#add-share-provider {
list-style-image: url(chrome://browser/skin/menuPanel-small@2x.png);
-moz-image-region: rect(0px, 192px, 32px, 160px);
}
#loop-call-button > .toolbarbutton-badge-container {
list-style-image: url("chrome://browser/skin/loop/toolbar@2x.png");
-moz-image-region: rect(0, 36px, 36px, 0);

View File

@ -249,6 +249,10 @@
color: #f5f7fa; /* Light foreground text */
}
.theme-dark .trace-item.black-boxed {
color: rgba(128,128,128,0.4);
}
.theme-dark .trace-item.selected-matching {
background-color: rgba(29,79,115,.4); /* Select highlight blue at 40% alpha */
}
@ -284,6 +288,10 @@
color: #292e33; /* Dark foreground text */
}
.theme-light .trace-item.black-boxed {
color: rgba(128,128,128,0.4);
}
.theme-light .trace-item.selected-matching {
background-color: rgba(76,158,217,.4); /* Select highlight blue at 40% alpha */
}

View File

@ -218,3 +218,8 @@ toolbarpaletteitem[place="palette"] > #zoom-controls > #zoom-out-button {
toolbarpaletteitem[place="palette"] > #zoom-controls > #zoom-in-button {
-moz-image-region: rect(0px, 96px, 16px, 80px);
}
#add-share-provider {
list-style-image: url(chrome://browser/skin/menuPanel-small.png);
-moz-image-region: rect(0px, 96px, 16px, 80px);
}

View File

@ -175,6 +175,7 @@ DataReportingService.prototype = Object.freeze({
// The instance installs its own shutdown observers. So, we just
// fire and forget: it will clean itself up.
let reporter = this.healthReporter;
this.policy.ensureUserNotified();
}.bind(this),
}, delayInterval, this.timer.TYPE_ONE_SHOT);

View File

@ -3,14 +3,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pref("datareporting.policy.dataSubmissionEnabled", true);
pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
pref("datareporting.policy.dataSubmissionPolicyBypassAcceptance", false);
pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "0");
pref("datareporting.policy.dataSubmissionPolicyResponseType", "");
pref("datareporting.policy.dataSubmissionPolicyResponseTime", "0");
pref("datareporting.policy.firstRunTime", "0");
pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "0");
pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 0);
pref("datareporting.policy.dataSubmissionPolicyBypassNotification", false);
pref("datareporting.policy.currentPolicyVersion", 2);
pref("datareporting.policy.minimumPolicyVersion", 1);
pref("datareporting.policy.minimumPolicyVersion.channel-beta", 2);

View File

@ -26,22 +26,27 @@ this.MockPolicyListener = function MockPolicyListener() {
}
MockPolicyListener.prototype = {
onRequestDataUpload: function onRequestDataUpload(request) {
onRequestDataUpload: function (request) {
this._log.info("onRequestDataUpload invoked.");
this.requestDataUploadCount++;
this.lastDataRequest = request;
},
onRequestRemoteDelete: function onRequestRemoteDelete(request) {
onRequestRemoteDelete: function (request) {
this._log.info("onRequestRemoteDelete invoked.");
this.requestRemoteDeleteCount++;
this.lastRemoteDeleteRequest = request;
},
onNotifyDataPolicy: function onNotifyDataPolicy(request) {
this._log.info("onNotifyUser invoked.");
onNotifyDataPolicy: function (request, rejectMessage=null) {
this._log.info("onNotifyDataPolicy invoked.");
this.notifyUserCount++;
this.lastNotifyRequest = request;
if (rejectMessage) {
request.onUserNotifyFailed(rejectMessage);
} else {
request.onUserNotifyComplete();
}
},
};

View File

@ -3,14 +3,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This file is in transition. It was originally conceived to fulfill the
* needs of only Firefox Health Report. It is slowly being morphed into
* fulfilling the needs of all data reporting facilities in Gecko applications.
* As a result, some things feel a bit weird.
*
* DataReportingPolicy is both a driver for data reporting notification
* (a true policy) and the driver for FHR data submission. The latter should
* eventually be split into its own type and module.
* This file is in transition. Most of its content needs to be moved under
* /services/healthreport.
*/
#ifndef MERGED_COMPARTMENT
@ -20,6 +14,7 @@
this.EXPORTED_SYMBOLS = [
"DataSubmissionRequest", // For test use only.
"DataReportingPolicy",
"DATAREPORTING_POLICY_VERSION",
];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
@ -32,6 +27,11 @@ Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/UpdateChannel.jsm");
// The current policy version number. If the version number stored in the prefs
// is smaller than this, data upload will be disabled until the user is re-notified
// about the policy changes.
const DATAREPORTING_POLICY_VERSION = 1;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
// Used as a sanity lower bound for dates stored in prefs. This module was
@ -41,33 +41,15 @@ const OLDEST_ALLOWED_YEAR = 2012;
/**
* Represents a request to display data policy.
*
* Instances of this are created when the policy is requesting the user's
* approval to agree to the data submission policy.
*
* Receivers of these instances are expected to call one or more of the on*
* functions when events occur.
*
* When one of these requests is received, the first thing a callee should do
* is present notification to the user of the data policy. When the notice
* is displayed to the user, the callee should call `onUserNotifyComplete`.
* This begins a countdown timer that upon completion will signal implicit
* acceptance of the policy. If for whatever reason the callee could not
* display a notice, it should call `onUserNotifyFailed`.
*
* Once the user is notified of the policy, the callee has the option of
* signaling explicit user acceptance or rejection of the policy. They do this
* by calling `onUserAccept` or `onUserReject`, respectively. These functions
* are essentially proxies to
* DataReportingPolicy.{recordUserAcceptance,recordUserRejection}.
*
* If the user never explicitly accepts or rejects the policy, it will be
* implicitly accepted after a specified duration of time. The notice is
* expected to remain displayed even after implicit acceptance (in case the
* user is away from the device). So, no event signaling implicit acceptance
* is exposed.
*
* Receivers of instances of this type should treat it as a black box with
* the exception of the on* functions.
* If for whatever reason the callee could not display a notice,
* it should call `onUserNotifyFailed`.
*
* @param policy
* (DataReportingPolicy) The policy instance this request came from.
@ -78,17 +60,13 @@ function NotifyPolicyRequest(policy, deferred) {
this.policy = policy;
this.deferred = deferred;
}
NotifyPolicyRequest.prototype = {
NotifyPolicyRequest.prototype = Object.freeze({
/**
* Called when the user is notified of the policy.
*
* This starts a countdown timer that will eventually signify implicit
* acceptance of the data policy.
*/
onUserNotifyComplete: function onUserNotified() {
this.deferred.resolve();
return this.deferred.promise;
},
onUserNotifyComplete: function () {
return this.deferred.resolve();
},
/**
* Called when there was an error notifying the user about the policy.
@ -96,32 +74,10 @@ NotifyPolicyRequest.prototype = {
* @param error
* (Error) Explains what went wrong.
*/
onUserNotifyFailed: function onUserNotifyFailed(error) {
this.deferred.reject(error);
onUserNotifyFailed: function (error) {
return this.deferred.reject(error);
},
/**
* Called when the user agreed to the data policy.
*
* @param reason
* (string) How the user agreed to the policy.
*/
onUserAccept: function onUserAccept(reason) {
this.policy.recordUserAcceptance(reason);
},
/**
* Called when the user rejected the data policy.
*
* @param reason
* (string) How the user rejected the policy.
*/
onUserReject: function onUserReject(reason) {
this.policy.recordUserRejection(reason);
},
};
Object.freeze(NotifyPolicyRequest.prototype);
});
/**
* Represents a request to submit data.
@ -238,9 +194,7 @@ this.DataSubmissionRequest.prototype = Object.freeze({
* data collection practices.
* 5. User should have opportunity to react to this notification before
* data submission.
* 6. Display of notification without any explicit user action constitutes
* implicit consent after a certain duration of time.
* 7. If data submission fails, try at most 2 additional times before giving
* 6. If data submission fails, try at most 2 additional times before giving
* up on that day's submission.
*
* The listener passed into the instance must have the following properties
@ -289,19 +243,11 @@ this.DataReportingPolicy = function (prefs, healthReportPrefs, listener) {
this._prefs = prefs;
this._healthReportPrefs = healthReportPrefs;
this._listener = listener;
this._userNotifyPromise = null;
// If the policy version has changed, reset all preferences, so that
// the notification reappears.
let acceptedVersion = this._prefs.get("dataSubmissionPolicyAcceptedVersion");
if (typeof(acceptedVersion) == "number" &&
acceptedVersion < this.minimumPolicyVersion) {
this._log.info("policy version has changed - resetting all prefs");
// We don't want to delay the notification in this case.
let firstRunToRestore = this.firstRunDate;
this._prefs.resetBranch();
this.firstRunDate = firstRunToRestore.getTime() ?
firstRunToRestore : this.now();
} else if (!this.firstRunDate.getTime()) {
this._migratePrefs();
if (!this.firstRunDate.getTime()) {
// If we've never run before, record the current time.
this.firstRunDate = this.now();
}
@ -329,30 +275,12 @@ this.DataReportingPolicy = function (prefs, healthReportPrefs, listener) {
this.nextDataSubmissionDate = this._futureDate(MILLISECONDS_PER_DAY);
}
// Date at which we performed user notification of acceptance.
// This is an instance variable because implicit acceptance should only
// carry forward through a single application instance.
this._dataSubmissionPolicyNotifiedDate = null;
// Record when we last requested for submitted data to be sent. This is
// to avoid having multiple outstanding requests.
this._inProgressSubmissionRequest = null;
};
this.DataReportingPolicy.prototype = Object.freeze({
/**
* How long after first run we should notify about data submission.
*/
SUBMISSION_NOTIFY_INTERVAL_MSEC: 12 * 60 * 60 * 1000,
/**
* Time that must elapse with no user action for implicit acceptance.
*
* THERE ARE POTENTIAL LEGAL IMPLICATIONS OF CHANGING THIS VALUE. Check with
* Privacy and/or Legal before modifying.
*/
IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 8 * 60 * 60 * 1000,
/**
* How often to poll to see if we need to do something.
*
@ -393,13 +321,6 @@ this.DataReportingPolicy.prototype = Object.freeze({
60 * 60 * 1000,
],
/**
* State of user notification of data submission.
*/
STATE_NOTIFY_UNNOTIFIED: "not-notified",
STATE_NOTIFY_WAIT: "waiting",
STATE_NOTIFY_COMPLETE: "ok",
REQUIRED_LISTENERS: [
"onRequestDataUpload",
"onRequestRemoteDelete",
@ -422,22 +343,6 @@ this.DataReportingPolicy.prototype = Object.freeze({
OLDEST_ALLOWED_YEAR);
},
/**
* Short circuit policy checking and always assume acceptance.
*
* This shuld never be set by the user. Instead, it is a per-application or
* per-deployment default pref.
*/
get dataSubmissionPolicyBypassAcceptance() {
return this._prefs.get("dataSubmissionPolicyBypassAcceptance", false);
},
/**
* When the user was notified that data submission could occur.
*
* This is used for logging purposes. this._dataSubmissionPolicyNotifiedDate
* is what's used internally.
*/
get dataSubmissionPolicyNotifiedDate() {
return CommonUtils.getDatePref(this._prefs,
"dataSubmissionPolicyNotifiedTime", 0,
@ -450,46 +355,12 @@ this.DataReportingPolicy.prototype = Object.freeze({
value, OLDEST_ALLOWED_YEAR);
},
/**
* When the user accepted or rejected the data submission policy.
*
* If there was implicit acceptance, this will be set to the time of that.
*/
get dataSubmissionPolicyResponseDate() {
return CommonUtils.getDatePref(this._prefs,
"dataSubmissionPolicyResponseTime",
0, this._log, OLDEST_ALLOWED_YEAR);
get dataSubmissionPolicyBypassNotification() {
return this._prefs.get("dataSubmissionPolicyBypassNotification", false);
},
set dataSubmissionPolicyResponseDate(value) {
this._log.debug("Setting user notified reaction date: " + value);
CommonUtils.setDatePref(this._prefs,
"dataSubmissionPolicyResponseTime",
value, OLDEST_ALLOWED_YEAR);
},
/**
* Records the result of user notification of data submission policy.
*
* This is used for logging and diagnostics purposes. It can answer the
* question "how was data submission agreed to on this profile?"
*
* Not all values are defined by this type and can come from other systems.
*
* The value must be a string and should be something machine readable. e.g.
* "accept-user-clicked-ok-button-in-info-bar"
*/
get dataSubmissionPolicyResponseType() {
return this._prefs.get("dataSubmissionPolicyResponseType",
"none-recorded");
},
set dataSubmissionPolicyResponseType(value) {
if (typeof(value) != "string") {
throw new Error("Value must be a string. Got " + typeof(value));
}
this._prefs.set("dataSubmissionPolicyResponseType", value);
set dataSubmissionPolicyBypassNotification(value) {
return this._prefs.set("dataSubmissionPolicyBypassNotification", !!value);
},
/**
@ -507,6 +378,10 @@ this.DataReportingPolicy.prototype = Object.freeze({
this._prefs.set("dataSubmissionEnabled", !!value);
},
get currentPolicyVersion() {
return this._prefs.get("currentPolicyVersion", DATAREPORTING_POLICY_VERSION);
},
/**
* The minimum policy version which for dataSubmissionPolicyAccepted to
* to be valid.
@ -519,48 +394,21 @@ this.DataReportingPolicy.prototype = Object.freeze({
channelPref : this._prefs.get("minimumPolicyVersion", 1);
},
/**
* Whether the user has accepted that data submission can occur.
*
* This overrides dataSubmissionEnabled.
*/
get dataSubmissionPolicyAccepted() {
// Be conservative and default to false.
return this._prefs.get("dataSubmissionPolicyAccepted", false);
get dataSubmissionPolicyAcceptedVersion() {
return this._prefs.get("dataSubmissionPolicyAcceptedVersion", 0);
},
set dataSubmissionPolicyAccepted(value) {
this._prefs.set("dataSubmissionPolicyAccepted", !!value);
if (!!value) {
let currentPolicyVersion = this._prefs.get("currentPolicyVersion", 1);
this._prefs.set("dataSubmissionPolicyAcceptedVersion", currentPolicyVersion);
} else {
this._prefs.reset("dataSubmissionPolicyAcceptedVersion");
}
set dataSubmissionPolicyAcceptedVersion(value) {
this._prefs.set("dataSubmissionPolicyAcceptedVersion", value);
},
/**
* The state of user notification of the data policy.
*
* This must be DataReportingPolicy.STATE_NOTIFY_COMPLETE before data
* submission can occur.
*
* @return DataReportingPolicy.STATE_NOTIFY_* constant.
* Checks to see if the user has been notified about data submission
* @return {bool}
*/
get notifyState() {
if (this.dataSubmissionPolicyResponseDate.getTime()) {
return this.STATE_NOTIFY_COMPLETE;
}
// We get the local state - not the state from prefs - because we don't want
// a value from a previous application run to interfere. This prevents
// a scenario where notification occurs just before application shutdown and
// notification is displayed for shorter than the policy requires.
if (!this._dataSubmissionPolicyNotifiedDate) {
return this.STATE_NOTIFY_UNNOTIFIED;
}
return this.STATE_NOTIFY_WAIT;
get userNotifiedOfCurrentPolicy() {
return this.dataSubmissionPolicyNotifiedDate.getTime() > 0 &&
this.dataSubmissionPolicyAcceptedVersion >= this.currentPolicyVersion;
},
/**
@ -693,43 +541,6 @@ this.DataReportingPolicy.prototype = Object.freeze({
return this._healthReportPrefs.locked("uploadEnabled");
},
/**
* Record user acceptance of data submission policy.
*
* Data submission will not be allowed to occur until this is called.
*
* This is typically called through the `onUserAccept` property attached to
* the promise passed to `onUserNotify` in the policy listener. But, it can
* be called through other interfaces at any time and the call will have
* an impact on future data submissions.
*
* @param reason
* (string) How the user accepted the data submission policy.
*/
recordUserAcceptance: function recordUserAcceptance(reason="no-reason") {
this._log.info("User accepted data submission policy: " + reason);
this.dataSubmissionPolicyResponseDate = this.now();
this.dataSubmissionPolicyResponseType = "accepted-" + reason;
this.dataSubmissionPolicyAccepted = true;
},
/**
* Record user rejection of submission policy.
*
* Data submission will not be allowed to occur if this is called.
*
* This is typically called through the `onUserReject` property attached to
* the promise passed to `onUserNotify` in the policy listener. But, it can
* be called through other interfaces at any time and the call will have an
* impact on future data submissions.
*/
recordUserRejection: function recordUserRejection(reason="no-reason") {
this._log.info("User rejected data submission policy: " + reason);
this.dataSubmissionPolicyResponseDate = this.now();
this.dataSubmissionPolicyResponseType = "rejected-" + reason;
this.dataSubmissionPolicyAccepted = false;
},
/**
* Record the user's intent for whether FHR should upload data.
*
@ -882,19 +693,13 @@ this.DataReportingPolicy.prototype = Object.freeze({
return;
}
// If the user hasn't responded to the data policy, don't do anything.
if (!this.ensureNotifyResponse(now)) {
if (!this.ensureUserNotified()) {
this._log.warn("The user has not been notified about the data submission " +
"policy. Not attempting upload.");
return;
}
// User has opted out of data submission.
if (!this.dataSubmissionPolicyAccepted && !this.dataSubmissionPolicyBypassAcceptance) {
this._log.debug("Data submission has been disabled per user request.");
return;
}
// User has responded to data policy and data submission is enabled. Now
// comes the scheduling part.
// Data submission is allowed to occur. Now comes the scheduling part.
if (nowT < nextSubmissionDate.getTime()) {
this._log.debug("Next data submission is scheduled in the future: " +
@ -906,82 +711,62 @@ this.DataReportingPolicy.prototype = Object.freeze({
},
/**
* Ensure user has responded to data submission policy.
* Ensure that the data policy notification has been displayed.
*
* This must be called before data submission. If the policy has not been
* responded to, data submission must not occur.
* displayed, data submission must not occur.
*
* @return bool Whether user has responded to data policy.
* @return bool Whether the notification has been displayed.
*/
ensureNotifyResponse: function ensureNotifyResponse(now) {
if (this.dataSubmissionPolicyBypassAcceptance) {
ensureUserNotified: function () {
if (this.userNotifiedOfCurrentPolicy || this.dataSubmissionPolicyBypassNotification) {
return true;
}
let notifyState = this.notifyState;
if (notifyState == this.STATE_NOTIFY_UNNOTIFIED) {
let notifyAt = new Date(this.firstRunDate.getTime() +
this.SUBMISSION_NOTIFY_INTERVAL_MSEC);
if (now.getTime() < notifyAt.getTime()) {
this._log.debug("Don't have to notify about data submission yet.");
return false;
}
let onComplete = function onComplete() {
this._log.info("Data submission notification presented.");
let now = this.now();
this._dataSubmissionPolicyNotifiedDate = now;
this.dataSubmissionPolicyNotifiedDate = now;
}.bind(this);
let deferred = Promise.defer();
deferred.promise.then(onComplete, (error) => {
this._log.warn("Data policy notification presentation failed: " +
CommonUtils.exceptionStr(error));
});
this._log.info("Requesting display of data policy.");
let request = new NotifyPolicyRequest(this, deferred);
try {
this._listener.onNotifyDataPolicy(request);
} catch (ex) {
this._log.warn("Exception when calling onNotifyDataPolicy: " +
CommonUtils.exceptionStr(ex));
}
// The user has not been notified yet, but is in the process of being notified.
if (this._userNotifyPromise) {
return false;
}
// We're waiting for user action or implicit acceptance after display.
if (notifyState == this.STATE_NOTIFY_WAIT) {
// Check for implicit acceptance.
let implicitAcceptance =
this._dataSubmissionPolicyNotifiedDate.getTime() +
this.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC;
let deferred = Promise.defer();
deferred.promise.then((function onSuccess() {
this._recordDataPolicyNotification(this.now(), this.currentPolicyVersion);
this._userNotifyPromise = null;
}).bind(this), ((error) => {
this._log.warn("Data policy notification presentation failed: " +
CommonUtils.exceptionStr(error));
this._userNotifyPromise = null;
}).bind(this));
this._log.debug("Now: " + now.getTime());
this._log.debug("Will accept: " + implicitAcceptance);
if (now.getTime() < implicitAcceptance) {
this._log.debug("Still waiting for reaction or implicit acceptance. " +
"Now: " + now.getTime() + " < " +
"Accept: " + implicitAcceptance);
return false;
}
this.recordUserAcceptance("implicit-time-elapsed");
return true;
this._log.info("Requesting display of data policy.");
let request = new NotifyPolicyRequest(this, deferred);
try {
this._listener.onNotifyDataPolicy(request);
} catch (ex) {
this._log.warn("Exception when calling onNotifyDataPolicy: " +
CommonUtils.exceptionStr(ex));
}
// If this happens, we have a coding error in this file.
if (notifyState != this.STATE_NOTIFY_COMPLETE) {
throw new Error("Unknown notification state: " + notifyState);
}
this._userNotifyPromise = deferred.promise;
return true;
return false;
},
_recordDataPolicyNotification: function (date, version) {
this._log.debug("Recording data policy notification to version " + version +
" on date " + date);
this.dataSubmissionPolicyNotifiedDate = date;
this.dataSubmissionPolicyAcceptedVersion = version;
},
_migratePrefs: function () {
// Current prefs are mostly the same than the old ones, except for some deprecated ones.
this._prefs.reset([
"dataSubmissionPolicyAccepted",
"dataSubmissionPolicyBypassAcceptance",
"dataSubmissionPolicyResponseType",
"dataSubmissionPolicyResponseTime"
]);
},
_processInProgressSubmission: function _processInProgressSubmission() {

View File

@ -9,6 +9,7 @@ Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
Cu.import("resource://gre/modules/UpdateChannel.jsm");
Cu.import("resource://gre/modules/Task.jsm");
function getPolicy(name,
aCurrentPolicyVersion = 1,
@ -37,6 +38,22 @@ function getPolicy(name,
return [policy, policyPrefs, healthReportPrefs, listener];
}
/**
* Ensure that the notification has been displayed to the user therefore having
* policy.ensureUserNotified() === true, which will allow for a successful
* data upload and afterwards does a call to policy.checkStateAndTrigger()
* @param {Policy} policy
* @return {Promise}
*/
function ensureUserNotifiedAndTrigger(policy) {
return Task.spawn(function* ensureUserNotifiedAndTrigger () {
policy.ensureUserNotified();
yield policy._listener.lastNotifyRequest.deferred.promise;
do_check_true(policy.userNotifiedOfCurrentPolicy);
policy.checkStateAndTrigger();
});
}
function defineNow(policy, now) {
print("Adjusting fake system clock to " + now);
Object.defineProperty(policy, "now", {
@ -66,7 +83,8 @@ add_test(function test_constructor() {
let tomorrow = Date.now() + 24 * 60 * 60 * 1000;
do_check_true(tomorrow - policy.nextDataSubmissionDate.getTime() < 1000);
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
do_check_eq(policy.dataSubmissionPolicyAcceptedVersion, 0);
do_check_false(policy.userNotifiedOfCurrentPolicy);
run_next_test();
});
@ -81,29 +99,23 @@ add_test(function test_prefs() {
do_check_eq(policyPrefs.get("firstRunTime"), nowT);
do_check_eq(policy.firstRunDate.getTime(), nowT);
policy.dataSubmissionPolicyNotifiedDate= now;
policy.dataSubmissionPolicyNotifiedDate = now;
do_check_eq(policyPrefs.get("dataSubmissionPolicyNotifiedTime"), nowT);
do_check_neq(policy.dataSubmissionPolicyNotifiedDate, null);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), nowT);
policy.dataSubmissionPolicyResponseDate = now;
do_check_eq(policyPrefs.get("dataSubmissionPolicyResponseTime"), nowT);
do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), nowT);
policy.dataSubmissionPolicyResponseType = "type-1";
do_check_eq(policyPrefs.get("dataSubmissionPolicyResponseType"), "type-1");
do_check_eq(policy.dataSubmissionPolicyResponseType, "type-1");
policy.dataSubmissionEnabled = false;
do_check_false(policyPrefs.get("dataSubmissionEnabled", true));
do_check_false(policy.dataSubmissionEnabled);
policy.dataSubmissionPolicyAccepted = false;
do_check_false(policyPrefs.get("dataSubmissionPolicyAccepted", true));
do_check_false(policy.dataSubmissionPolicyAccepted);
let new_version = DATAREPORTING_POLICY_VERSION + 1;
policy.dataSubmissionPolicyAcceptedVersion = new_version;
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), new_version);
do_check_false(policy.dataSubmissionPolicyBypassAcceptance);
policyPrefs.set("dataSubmissionPolicyBypassAcceptance", true);
do_check_true(policy.dataSubmissionPolicyBypassAcceptance);
do_check_false(policy.dataSubmissionPolicyBypassNotification);
policy.dataSubmissionPolicyBypassNotification = true;
do_check_true(policy.dataSubmissionPolicyBypassNotification);
do_check_true(policyPrefs.get("dataSubmissionPolicyBypassNotification"));
policy.lastDataSubmissionRequestedDate = now;
do_check_eq(hrPrefs.get("lastDataSubmissionRequestedTime"), nowT);
@ -142,153 +154,78 @@ add_test(function test_prefs() {
run_next_test();
});
add_test(function test_notify_state_prefs() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notify_state_prefs");
add_task(function test_migratePrefs () {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("migratePrefs");
let outdated_prefs = {
dataSubmissionPolicyAccepted: true,
dataSubmissionPolicyBypassAcceptance: true,
dataSubmissionPolicyResponseType: "something",
dataSubmissionPolicyResponseTime: Date.now() + "",
};
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
policy._dataSubmissionPolicyNotifiedDate = new Date();
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
policy.dataSubmissionPolicyResponseDate = new Date();
policy._dataSubmissionPolicyNotifiedDate = null;
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
run_next_test();
// Test removal of old prefs.
for (let name in outdated_prefs) {
policyPrefs.set(name, outdated_prefs[name]);
}
policy._migratePrefs();
for (let name in outdated_prefs) {
do_check_false(policyPrefs.has(name));
}
});
add_task(function test_initial_submission_notification() {
add_task(function test_userNotifiedOfCurrentPolicy () {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("initial_submission_notification");
do_check_eq(listener.notifyUserCount, 0);
// Fresh instances should not do anything initially.
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 0);
// We still shouldn't notify up to the millisecond before the barrier.
defineNow(policy, new Date(policy.firstRunDate.getTime() +
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC - 1));
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 0);
do_check_null(policy._dataSubmissionPolicyNotifiedDate);
do_check_false(policy.userNotifiedOfCurrentPolicy,
"The initial state should be unnotified.");
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
// We have crossed the threshold. We should see notification.
defineNow(policy, new Date(policy.firstRunDate.getTime() +
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC));
policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION;
do_check_false(policy.userNotifiedOfCurrentPolicy,
"The default state of the date should have a time of 0 and it should therefore fail");
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0,
"Updating the accepted version should not set a notified date.");
policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
do_check_true(policy.userNotifiedOfCurrentPolicy,
"Using the proper API causes user notification to report as true.");
// It is assumed that later versions of the policy will incorporate previous
// ones, therefore this should also return true.
policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION + 1;
do_check_true(policy.userNotifiedOfCurrentPolicy, 'A future version of the policy should pass.');
policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION - 1;
do_check_false(policy.userNotifiedOfCurrentPolicy, 'A previous version of the policy should fail.');
});
add_task(function* test_notification_displayed () {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_accept_displayed");
do_check_eq(listener.requestDataUploadCount, 0);
do_check_eq(listener.notifyUserCount, 0);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
// Uploads will trigger user notifications as needed.
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
yield listener.lastNotifyRequest.onUserNotifyComplete();
do_check_true(policy._dataSubmissionPolicyNotifiedDate instanceof Date);
do_check_eq(listener.requestDataUploadCount, 0);
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.notifyUserCount, 1);
do_check_true(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(),
policy._dataSubmissionPolicyNotifiedDate.getTime());
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
do_check_true(policy.userNotifiedOfCurrentPolicy);
});
add_test(function test_bypass_acceptance() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("bypass_acceptance");
policyPrefs.set("dataSubmissionPolicyBypassAcceptance", true);
do_check_false(policy.dataSubmissionPolicyAccepted);
do_check_true(policy.dataSubmissionPolicyBypassAcceptance);
defineNow(policy, new Date(policy.nextDataSubmissionDate.getTime()));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
run_next_test();
});
add_task(function test_notification_implicit_acceptance() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_implicit_acceptance");
let now = new Date(policy.nextDataSubmissionDate.getTime() -
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
defineNow(policy, now);
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
yield listener.lastNotifyRequest.onUserNotifyComplete();
do_check_eq(policy.dataSubmissionPolicyResponseType, "none-recorded");
do_check_true(5000 < policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC);
defineNow(policy, new Date(now.getTime() + 5000));
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), 0);
do_check_eq(policy.dataSubmissionPolicyResponseType, "none-recorded");
defineNow(policy, new Date(now.getTime() + policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC + 1));
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), policy.now().getTime());
do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-implicit-time-elapsed");
});
add_task(function test_notification_rejected() {
// User notification failed. We should not record it as being presented.
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_failed");
let now = new Date(policy.nextDataSubmissionDate.getTime() -
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
defineNow(policy, now);
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, 1);
yield listener.lastNotifyRequest.onUserNotifyFailed(new Error("testing failed."));
do_check_null(policy._dataSubmissionPolicyNotifiedDate);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
});
add_task(function test_notification_accepted() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_accepted");
let now = new Date(policy.nextDataSubmissionDate.getTime() -
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
defineNow(policy, now);
policy.checkStateAndTrigger();
yield listener.lastNotifyRequest.onUserNotifyComplete();
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
do_check_false(policy.dataSubmissionPolicyAccepted);
listener.lastNotifyRequest.onUserNotifyComplete();
listener.lastNotifyRequest.onUserAccept("foo-bar");
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-foo-bar");
do_check_true(policy.dataSubmissionPolicyAccepted);
do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), now.getTime());
});
add_task(function test_notification_rejected() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_rejected");
let now = new Date(policy.nextDataSubmissionDate.getTime() -
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
defineNow(policy, now);
policy.checkStateAndTrigger();
yield listener.lastNotifyRequest.onUserNotifyComplete();
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
do_check_false(policy.dataSubmissionPolicyAccepted);
listener.lastNotifyRequest.onUserReject();
do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
do_check_eq(policy.dataSubmissionPolicyResponseType, "rejected-no-reason");
do_check_false(policy.dataSubmissionPolicyAccepted);
// No requests for submission should occur if user has rejected.
defineNow(policy, new Date(policy.nextDataSubmissionDate.getTime() + 10000));
add_task(function* test_submission_kill_switch() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_kill_switch");
policy.nextDataSubmissionDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 0);
});
add_test(function test_submission_kill_switch() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_kill_switch");
policy.firstRunDate = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
policy.nextDataSubmissionDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.recordUserAcceptance("accept-old-ack");
do_check_true(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
policy.checkStateAndTrigger();
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy,
@ -296,39 +233,32 @@ add_test(function test_submission_kill_switch() {
policy.dataSubmissionEnabled = false;
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 1);
run_next_test();
});
add_test(function test_upload_kill_switch() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("upload_kill_switch");
add_task(function* test_upload_kill_switch() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("upload_kill_switch");
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
yield ensureUserNotifiedAndTrigger(policy);
defineNow(policy, policy.nextDataSubmissionDate);
// So that we don't trigger deletions, which cause uploads to be delayed.
hrPrefs.ignore("uploadEnabled", policy.uploadEnabledObserver);
policy.healthReportUploadEnabled = false;
policy.checkStateAndTrigger();
yield policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 0);
policy.healthReportUploadEnabled = true;
policy.checkStateAndTrigger();
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
run_next_test();
});
add_test(function test_data_submission_no_data() {
add_task(function* test_data_submission_no_data() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_no_data");
policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.dataSubmissionPolicyAccepted = true;
let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
defineNow(policy, now);
do_check_eq(listener.requestDataUploadCount, 0);
policy.checkStateAndTrigger();
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
listener.lastDataRequest.onNoDataAvailable();
@ -336,20 +266,16 @@ add_test(function test_data_submission_no_data() {
defineNow(policy, new Date(now.getTime() + 155 * 60 * 1000));
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 2);
});
run_next_test();
});
add_task(function test_data_submission_submit_failure_hard() {
add_task(function* test_data_submission_submit_failure_hard() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_submit_failure_hard");
policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.dataSubmissionPolicyAccepted = true;
let nextDataSubmissionDate = policy.nextDataSubmissionDate;
let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
defineNow(policy, now);
policy.checkStateAndTrigger();
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
yield listener.lastDataRequest.onSubmissionFailureHard();
do_check_eq(listener.lastDataRequest.state,
@ -363,30 +289,27 @@ add_task(function test_data_submission_submit_failure_hard() {
do_check_eq(listener.requestDataUploadCount, 1);
});
add_task(function test_data_submission_submit_try_again() {
add_task(function* test_data_submission_submit_try_again() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_failure_soft");
policy.recordUserAcceptance();
let nextDataSubmissionDate = policy.nextDataSubmissionDate;
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
policy.checkStateAndTrigger();
yield ensureUserNotifiedAndTrigger(policy);
yield listener.lastDataRequest.onSubmissionFailureSoft();
do_check_eq(policy.nextDataSubmissionDate.getTime(),
nextDataSubmissionDate.getTime() + 15 * 60 * 1000);
});
add_task(function test_submission_daily_scheduling() {
add_task(function* test_submission_daily_scheduling() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_daily_scheduling");
policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.dataSubmissionPolicyAccepted = true;
let nextDataSubmissionDate = policy.nextDataSubmissionDate;
// Skip ahead to next submission date. We should get a submission request.
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
policy.checkStateAndTrigger();
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), now.getTime());
@ -414,18 +337,17 @@ add_task(function test_submission_daily_scheduling() {
new Date(nextScheduled.getTime() + 24 * 60 * 60 * 1000 + 200).getTime());
});
add_test(function test_submission_far_future_scheduling() {
add_task(function* test_submission_far_future_scheduling() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_far_future_scheduling");
let now = new Date(Date.now() - 24 * 60 * 60 * 1000);
defineNow(policy, now);
policy.recordUserAcceptance();
now = new Date();
defineNow(policy, now);
yield ensureUserNotifiedAndTrigger(policy);
let nextDate = policy._futureDate(3 * 24 * 60 * 60 * 1000 - 1);
policy.nextDataSubmissionDate = nextDate;
policy.checkStateAndTrigger();
do_check_true(policy.dataSubmissionPolicyAcceptedVersion >= DATAREPORTING_POLICY_VERSION);
do_check_eq(listener.requestDataUploadCount, 0);
do_check_eq(policy.nextDataSubmissionDate.getTime(), nextDate.getTime());
@ -434,21 +356,17 @@ add_test(function test_submission_far_future_scheduling() {
do_check_eq(listener.requestDataUploadCount, 0);
do_check_eq(policy.nextDataSubmissionDate.getTime(),
policy._futureDate(24 * 60 * 60 * 1000).getTime());
run_next_test();
});
add_task(function test_submission_backoff() {
add_task(function* test_submission_backoff() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_backoff");
do_check_eq(policy.FAILURE_BACKOFF_INTERVALS.length, 2);
policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.dataSubmissionPolicyAccepted = true;
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
policy.checkStateAndTrigger();
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
do_check_eq(policy.currentDaySubmissionFailureCount, 0);
@ -499,15 +417,13 @@ add_task(function test_submission_backoff() {
});
// Ensure that only one submission request can be active at a time.
add_test(function test_submission_expiring() {
add_task(function* test_submission_expiring() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_expiring");
policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
policy.dataSubmissionPolicyAccepted = true;
let nextDataSubmission = policy.nextDataSubmissionDate;
let now = new Date(policy.nextDataSubmissionDate.getTime());
defineNow(policy, now);
policy.checkStateAndTrigger();
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy, new Date(now.getTime() + 500));
policy.checkStateAndTrigger();
@ -518,11 +434,9 @@ add_test(function test_submission_expiring() {
policy.checkStateAndTrigger();
do_check_eq(listener.requestDataUploadCount, 2);
run_next_test();
});
add_task(function test_delete_remote_data() {
add_task(function* test_delete_remote_data() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data");
do_check_false(policy.pendingDeleteRemoteData);
@ -546,15 +460,13 @@ add_task(function test_delete_remote_data() {
});
// Ensure that deletion requests take priority over regular data submission.
add_test(function test_delete_remote_data_priority() {
add_task(function* test_delete_remote_data_priority() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_priority");
let now = new Date();
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
defineNow(policy, new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000));
policy.checkStateAndTrigger();
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
policy._inProgressSubmissionRequest = null;
@ -563,16 +475,12 @@ add_test(function test_delete_remote_data_priority() {
do_check_eq(listener.requestRemoteDeleteCount, 1);
do_check_eq(listener.requestDataUploadCount, 1);
run_next_test();
});
add_test(function test_delete_remote_data_backoff() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_backoff");
let now = new Date();
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
defineNow(policy, now);
policy.nextDataSubmissionDate = now;
policy.deleteRemoteData();
@ -600,15 +508,12 @@ add_test(function test_delete_remote_data_backoff() {
// If we request delete while an upload is in progress, delete should be
// scheduled immediately after upload.
add_task(function test_delete_remote_data_in_progress_upload() {
add_task(function* test_delete_remote_data_in_progress_upload() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_in_progress_upload");
let now = new Date();
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
defineNow(policy, policy.nextDataSubmissionDate);
policy.checkStateAndTrigger();
yield ensureUserNotifiedAndTrigger(policy);
do_check_eq(listener.requestDataUploadCount, 1);
defineNow(policy, policy._futureDate(50 * 1000));
@ -654,7 +559,6 @@ add_test(function test_polling() {
if (count >= 2) {
policy.stopPolling();
do_check_eq(listener.notifyUserCount, 0);
do_check_eq(listener.requestDataUploadCount, 0);
run_next_test();
@ -672,79 +576,7 @@ add_test(function test_polling() {
policy.startPolling();
});
// Ensure that implicit acceptance of policy is resolved through polling.
//
// This is probably covered by other tests. But, it's best to have explicit
// coverage from a higher-level.
add_test(function test_polling_implicit_acceptance() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("polling_implicit_acceptance");
// Redefine intervals with shorter, test-friendly values.
Object.defineProperty(policy, "POLL_INTERVAL_MSEC", {
value: 250,
});
Object.defineProperty(policy, "IMPLICIT_ACCEPTANCE_INTERVAL_MSEC", {
value: 700,
});
let count = 0;
// Track JS elapsed time, so we can decide if we've waited for enough ticks.
let start;
Object.defineProperty(policy, "checkStateAndTrigger", {
value: function CheckStateAndTriggerProxy() {
count++;
let now = Date.now();
let delta = now - start;
print("checkStateAndTrigger count: " + count + ", now " + now +
", delta " + delta);
// Account for some slack.
DataReportingPolicy.prototype.checkStateAndTrigger.call(policy);
// What should happen on different invocations:
//
// 1) We are inside the prompt interval so user gets prompted.
// 2) still ~300ms away from implicit acceptance
// 3) still ~50ms away from implicit acceptance
// 4) Implicit acceptance recorded. Data submission requested.
// 5) Request still pending. No new submission requested.
//
// Note that, due to the inaccuracy of timers, 4 might not happen until 5
// firings have occurred. Yay. So we watch times, not just counts.
do_check_eq(listener.notifyUserCount, 1);
if (count == 1) {
listener.lastNotifyRequest.onUserNotifyComplete();
}
if (delta <= (policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC + policy.POLL_INTERVAL_MSEC)) {
do_check_false(policy.dataSubmissionPolicyAccepted);
do_check_eq(listener.requestDataUploadCount, 0);
} else if (count > 3) {
do_check_true(policy.dataSubmissionPolicyAccepted);
do_check_eq(policy.dataSubmissionPolicyResponseType,
"accepted-implicit-time-elapsed");
do_check_eq(listener.requestDataUploadCount, 1);
}
if ((count > 4) && policy.dataSubmissionPolicyAccepted) {
do_check_eq(listener.requestDataUploadCount, 1);
policy.stopPolling();
run_next_test();
}
}
});
policy.firstRunDate = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000);
policy.nextDataSubmissionDate = new Date(Date.now());
start = Date.now();
policy.startPolling();
});
add_task(function test_record_health_report_upload_enabled() {
add_task(function* test_record_health_report_upload_enabled() {
let [policy, policyPrefs, hrPrefs, listener] = getPolicy("record_health_report_upload_enabled");
// Preconditions.
@ -791,10 +623,10 @@ add_test(function test_pref_change_initiates_deletion() {
hrPrefs.set("uploadEnabled", false);
});
add_task(function* test_policy_version() {
let policy, policyPrefs, hrPrefs, listener, now, firstRunTime;
function createPolicy(shouldBeAccepted = false,
function createPolicy(shouldBeNotified = false,
currentPolicyVersion = 1, minimumPolicyVersion = 1,
branchMinimumVersionOverride) {
[policy, policyPrefs, hrPrefs, listener] =
@ -804,8 +636,7 @@ add_task(function* test_policy_version() {
if (firstRun) {
firstRunTime = policy.firstRunDate.getTime();
do_check_true(firstRunTime > 0);
now = new Date(policy.firstRunDate.getTime() +
policy.SUBMISSION_NOTIFY_INTERVAL_MSEC);
now = new Date(policy.firstRunDate.getTime());
}
else {
// The first-run time should not be reset even after policy-version
@ -813,23 +644,18 @@ add_task(function* test_policy_version() {
do_check_eq(policy.firstRunDate.getTime(), firstRunTime);
}
defineNow(policy, now);
do_check_eq(policy.dataSubmissionPolicyAccepted, shouldBeAccepted);
do_check_eq(policy.userNotifiedOfCurrentPolicy, shouldBeNotified);
}
function* triggerPolicyCheckAndEnsureNotified(notified = true, accept = true) {
function* triggerPolicyCheckAndEnsureNotified(notified = true) {
policy.checkStateAndTrigger();
do_check_eq(listener.notifyUserCount, Number(notified));
if (notified) {
yield listener.lastNotifyRequest.onUserNotifyComplete();
if (accept) {
listener.lastNotifyRequest.onUserAccept("because,");
do_check_true(policy.dataSubmissionPolicyAccepted);
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
policyPrefs.get("currentPolicyVersion"));
}
else {
do_check_false(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
}
policy.ensureUserNotified();
yield listener.lastNotifyRequest.deferred.promise;
do_check_true(policy.userNotifiedOfCurrentPolicy);
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
policyPrefs.get("currentPolicyVersion"));
}
}
@ -844,16 +670,16 @@ add_task(function* test_policy_version() {
// version must be changed.
let currentPolicyVersion = policyPrefs.get("currentPolicyVersion");
let minimumPolicyVersion = policyPrefs.get("minimumPolicyVersion");
createPolicy(true, ++currentPolicyVersion, minimumPolicyVersion);
yield triggerPolicyCheckAndEnsureNotified(false);
do_check_true(policy.dataSubmissionPolicyAccepted);
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
minimumPolicyVersion);
createPolicy(false, ++currentPolicyVersion, minimumPolicyVersion);
yield triggerPolicyCheckAndEnsureNotified(true);
do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), currentPolicyVersion);
// Increase the minimum policy version and check if we're notified.
createPolicy(false, currentPolicyVersion, ++minimumPolicyVersion);
do_check_false(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
yield triggerPolicyCheckAndEnsureNotified();
createPolicy(true, currentPolicyVersion, ++minimumPolicyVersion);
do_check_true(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
yield triggerPolicyCheckAndEnsureNotified(false);
// Test increasing the minimum version just on the current channel.
createPolicy(true, currentPolicyVersion, minimumPolicyVersion);

View File

@ -1260,8 +1260,8 @@ this.HealthReporter.prototype = Object.freeze({
* Whether this instance will upload data to a server.
*/
get willUploadData() {
return this._policy.dataSubmissionPolicyAccepted &&
this._policy.healthReportUploadEnabled;
return this._policy.userNotifiedOfCurrentPolicy &&
this._policy.healthReportUploadEnabled;
},
/**
@ -1321,8 +1321,8 @@ this.HealthReporter.prototype = Object.freeze({
// Need to capture this before we call the parent else it's always
// set.
let inShutdown = this._shutdownRequested;
let result;
try {
result = AbstractHealthReporter.prototype._onInitError.call(this, error);
} catch (ex) {
@ -1335,8 +1335,8 @@ this.HealthReporter.prototype = Object.freeze({
// startup errors is important. And, they should not occur with much
// frequency in the wild. So, it shouldn't be too big of a deal.
if (!inShutdown &&
this._policy.ensureNotifyResponse(new Date()) &&
this._policy.healthReportUploadEnabled) {
this._policy.healthReportUploadEnabled &&
this._policy.ensureUserNotified()) {
// We don't care about what happens to this request. It's best
// effort.
let request = {

View File

@ -25,6 +25,7 @@ Cu.import("resource://gre/modules/services-common/utils.js");
Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
let APP_INFO = {
@ -190,18 +191,16 @@ this.getHealthReporter = function (name, uri=DUMMY_URI, inspected=false) {
let reporter;
let policyPrefs = new Preferences(branch + "policy.");
let policy = new DataReportingPolicy(policyPrefs, prefs, {
onRequestDataUpload: function (request) {
reporter.requestDataUpload(request);
},
onNotifyDataPolicy: function (request) { },
onRequestRemoteDelete: function (request) {
reporter.deleteRemoteData(request);
},
});
let listener = new MockPolicyListener();
listener.onRequestDataUpload = function (request) {
reporter.requestDataUpload(request);
MockPolicyListener.prototype.onRequestDataUpload.call(this, request);
}
listener.onRequestRemoteDelete = function (request) {
reporter.deleteRemoteData(request);
MockPolicyListener.prototype.onRequestRemoteDelete.call(this, request);
}
let policy = new DataReportingPolicy(policyPrefs, prefs, listener);
let type = inspected ? InspectedHealthReporter : HealthReporter;
reporter = new type(branch + "healthreport.", policy, null,
"state-" + name + ".json");

View File

@ -92,6 +92,21 @@ function getHealthReportProviderValues(reporter, day=null) {
});
}
/*
* Ensure that the notification has been displayed to the user therefore having
* reporter._policy.userNotifiedOfCurrentPolicy === true, which will allow for a
* successful data upload.
* @param {HealthReporter} reporter
* @return {Promise}
*/
function ensureUserNotified (reporter) {
return Task.spawn(function* ensureUserNotified () {
reporter._policy.ensureUserNotified();
yield reporter._policy._listener.lastNotifyRequest.deferred.promise;
do_check_true(reporter._policy.userNotifiedOfCurrentPolicy);
});
}
function run_test() {
run_next_test();
}
@ -673,9 +688,8 @@ add_task(function test_recurring_daily_pings() {
let policy = reporter._policy;
defineNow(policy, policy._futureDate(-24 * 60 * 68 * 1000));
policy.recordUserAcceptance();
defineNow(policy, policy.nextDataSubmissionDate);
yield ensureUserNotified(reporter);
let promise = policy.checkStateAndTrigger();
do_check_neq(promise, null);
yield promise;
@ -712,8 +726,8 @@ add_task(function test_request_remote_data_deletion() {
try {
let policy = reporter._policy;
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
defineNow(policy, policy.nextDataSubmissionDate);
yield ensureUserNotified(reporter);
yield policy.checkStateAndTrigger();
let id = reporter.lastSubmitID;
do_check_neq(id, null);
@ -800,16 +814,12 @@ add_task(function test_policy_accept_reject() {
try {
let policy = reporter._policy;
do_check_false(policy.dataSubmissionPolicyAccepted);
do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
do_check_true(policy.dataSubmissionPolicyAcceptedVersion < DATAREPORTING_POLICY_VERSION);
do_check_false(reporter.willUploadData);
policy.recordUserAcceptance();
do_check_true(policy.dataSubmissionPolicyAccepted);
yield ensureUserNotified(reporter);
do_check_true(reporter.willUploadData);
policy.recordUserRejection();
do_check_false(policy.dataSubmissionPolicyAccepted);
do_check_false(reporter.willUploadData);
} finally {
yield reporter._shutdown();
yield shutdownServer(server);
@ -940,9 +950,9 @@ add_task(function test_upload_on_init_failure() {
},
});
reporter._policy.recordUserAcceptance();
let error = false;
try {
yield ensureUserNotified(reporter);
yield reporter.init();
} catch (ex) {
error = true;

View File

@ -128,8 +128,10 @@ user_pref("dom.use_xbl_scopes_for_remote_xul", true);
// Get network events.
user_pref("network.activity.blipIntervalMilliseconds", 250);
// Don't allow the Data Reporting service to prompt for policy acceptance.
user_pref("datareporting.policy.dataSubmissionPolicyBypassAcceptance", true);
// We do not wish to display datareporting policy notifications as it might
// cause other tests to fail. Tests that wish to test the notification functionality
// should explicitly disable this pref.
user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
// Point Firefox Health Report at a local server. We don't care if it actually
// works. It just can't hit the default production endpoint.

View File

@ -585,26 +585,24 @@ this.SocialService = {
action, [], options);
},
installProvider: function(aDOMDocument, data, installCallback, aBypassUserEnable=false) {
installProvider: function(aDOMDocument, data, installCallback, options={}) {
let manifest;
let installOrigin = aDOMDocument.nodePrincipal.origin;
if (data) {
let installType = getOriginActivationType(installOrigin);
// if we get data, we MUST have a valid manifest generated from the data
manifest = this._manifestFromData(installType, data, aDOMDocument.nodePrincipal);
if (!manifest)
throw new Error("SocialService.installProvider: service configuration is invalid from " + aDOMDocument.location.href);
let installType = getOriginActivationType(installOrigin);
// if we get data, we MUST have a valid manifest generated from the data
manifest = this._manifestFromData(installType, data, aDOMDocument.nodePrincipal);
if (!manifest)
throw new Error("SocialService.installProvider: service configuration is invalid from " + aDOMDocument.location.href);
let addon = new AddonWrapper(manifest);
if (addon && addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
throw new Error("installProvider: provider with origin [" +
installOrigin + "] is blocklisted");
// manifestFromData call above will enforce correct origin. To support
// activation from about: uris, we need to be sure to use the updated
// origin on the manifest.
installOrigin = manifest.origin;
}
let addon = new AddonWrapper(manifest);
if (addon && addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
throw new Error("installProvider: provider with origin [" +
installOrigin + "] is blocklisted");
// manifestFromData call above will enforce correct origin. To support
// activation from about: uris, we need to be sure to use the updated
// origin on the manifest.
installOrigin = manifest.origin;
let id = getAddonIDFromOrigin(installOrigin);
AddonManager.getAddonByID(id, function(aAddon) {
@ -613,7 +611,7 @@ this.SocialService = {
aAddon.userDisabled = false;
}
schedule(function () {
this._installProvider(aDOMDocument, manifest, aBypassUserEnable, aManifest => {
this._installProvider(aDOMDocument, manifest, options, aManifest => {
this._notifyProviderListeners("provider-installed", aManifest.origin);
installCallback(aManifest);
});
@ -621,43 +619,21 @@ this.SocialService = {
}.bind(this));
},
_installProvider: function(aDOMDocument, manifest, aBypassUserEnable, installCallback) {
let sourceURI = aDOMDocument.location.href;
let installOrigin = aDOMDocument.nodePrincipal.origin;
_installProvider: function(aDOMDocument, manifest, options, installCallback) {
if (!manifest)
throw new Error("Cannot install provider without manifest data");
let installType = getOriginActivationType(installOrigin);
let installer;
switch(installType) {
case "foreign":
if (!Services.prefs.getBoolPref("social.remote-install.enabled"))
throw new Error("Remote install of services is disabled");
if (!manifest)
throw new Error("Cannot install provider without manifest data");
let installType = getOriginActivationType(aDOMDocument.nodePrincipal.origin);
if (installType == "foreign" && !Services.prefs.getBoolPref("social.remote-install.enabled"))
throw new Error("Remote install of services is disabled");
installer = new AddonInstaller(sourceURI, manifest, installCallback);
this._showInstallNotification(aDOMDocument, installer);
break;
case "internal":
// double check here since "builtin" falls through this as well.
aBypassUserEnable = installType == "internal" && manifest.oneclick;
case "directory":
// a manifest is requried, and will have been vetted by reviewers. We
// also handle in-product installations without the verification step.
if (aBypassUserEnable) {
installer = new AddonInstaller(sourceURI, manifest, installCallback);
installer.install();
return;
}
// a manifest is required, we'll catch a missing manifest below.
if (!manifest)
throw new Error("Cannot install provider without manifest data");
installer = new AddonInstaller(sourceURI, manifest, installCallback);
this._showInstallNotification(aDOMDocument, installer);
break;
default:
throw new Error("SocialService.installProvider: Invalid install type "+installType+"\n");
break;
}
let installer = new AddonInstaller(aDOMDocument.location.href, manifest, installCallback);
let bypassPanel = options.bypassInstallPanel ||
(installType == "internal" && manifest.oneclick);
if (bypassPanel)
installer.install();
else
this._showInstallNotification(aDOMDocument, installer);
},
createWrapper: function(manifest) {

View File

@ -284,6 +284,12 @@ TracerActor.prototype = {
};
}
if (this._parent.threadActor && aFrame.script) {
packet.blackBoxed = this._parent.threadActor.sources.isBlackBoxed(aFrame.script.url);
} else {
packet.blackBoxed = false;
}
if (this._requestsForTraceType.callsite
&& aFrame.older
&& aFrame.older.script) {

View File

@ -175,9 +175,9 @@ function attachTestTab(aClient, aTitle, aCallback) {
// TabClient referring to the tab, and a ThreadClient referring to the
// thread.
function attachTestThread(aClient, aTitle, aCallback) {
attachTestTab(aClient, aTitle, function (aResponse, aTabClient) {
attachTestTab(aClient, aTitle, function (aTabResponse, aTabClient) {
function onAttach(aResponse, aThreadClient) {
aCallback(aResponse, aTabClient, aThreadClient);
aCallback(aResponse, aTabClient, aThreadClient, aTabResponse);
}
aTabClient.attachThread({
useSourceMaps: true,

View File

@ -0,0 +1,167 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Create 2 sources, A and B, B is black boxed. When calling functions A->B->A,
* verify that only traces from source B are black boxed.
*/
var gDebuggee;
var gClient;
var gTraceClient;
var gThreadClient;
function run_test()
{
initTestTracerServer();
gDebuggee = addTestGlobal("test-tracer-actor");
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect(function() {
attachTestThread(gClient, "test-tracer-actor",
function(aResponse, aTabClient, aThreadClient, aTabResponse) {
gThreadClient = aThreadClient;
gThreadClient.resume(function (aResponse) {
gClient.attachTracer(aTabResponse.traceActor,
function(aResponse, aTraceClient) {
gTraceClient = aTraceClient;
testTraces();
});
});
});
});
do_test_pending();
}
const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
const SOURCE_URL = "http://example.com/source.js";
const testTraces = Task.async(function* () {
// Read traces
const tracesStopped = promise.defer();
gClient.addListener("traces", (aEvent, { traces }) => {
for (let t of traces) {
check_trace(t);
}
tracesStopped.resolve();
});
yield startTrace();
evalSetup();
// Blackbox source
const sourcesResponse = yield getSources(gThreadClient);
let sourceClient = gThreadClient.source(
sourcesResponse.sources.filter(s => s.url == BLACK_BOXED_URL)[0]);
do_check_true(!sourceClient.isBlackBoxed,
"By default the source is not black boxed.");
yield blackBox(sourceClient);
do_check_true(sourceClient.isBlackBoxed);
evalTestCode();
yield tracesStopped.promise;
yield stopTrace();
finishClient(gClient);
});
function startTrace()
{
let deferred = promise.defer();
gTraceClient.startTrace(["depth", "name", "location"], null,
function() { deferred.resolve(); });
return deferred.promise;
}
function evalSetup()
{
Components.utils.evalInSandbox(
"" + function fnBlackBoxed(k) {
fnInner();
},
gDebuggee,
"1.8",
BLACK_BOXED_URL,
1
);
Components.utils.evalInSandbox(
"" + function fnOuter() {
fnBlackBoxed();
} + "\n" +
"" + function fnInner() {
[1].forEach(function noop() {});
},
gDebuggee,
"1.8",
SOURCE_URL,
1
);
}
function evalTestCode()
{
Components.utils.evalInSandbox(
"fnOuter();",
gDebuggee,
"1.8",
SOURCE_URL,
1
);
}
function stopTrace()
{
let deferred = promise.defer();
gTraceClient.stopTrace(null, function() { deferred.resolve(); });
return deferred.promise;
}
function check_trace({ type, sequence, depth, name, location, blackBoxed })
{
switch(sequence) {
// First two packets come from evalInSandbox in evalSetup
// The third packet comes from evalInSandbox in evalTestCode
case 0:
case 2:
case 4:
do_check_eq(name, "(global)");
do_check_eq(type, "enteredFrame");
break;
case 5:
do_check_eq(blackBoxed, false);
do_check_eq(name, "fnOuter");
break;
case 6:
do_check_eq(blackBoxed, true);
do_check_eq(name, "fnBlackBoxed");
break;
case 7:
do_check_eq(blackBoxed, false);
do_check_eq(name, "fnInner");
break;
case 8:
do_check_eq(blackBoxed, false);
do_check_eq(name, "noop");
break;
case 1: // evalInSandbox
case 3: // evalInSandbox
case 9: // noop
case 10: // fnInner
case 11: // fnBlackBoxed
case 12: // fnOuter
case 13: // evalInSandbox
do_check_eq(type, "exitedFrame");
break;
default:
// Should have covered all sequences.
do_check_true(false);
}
}

View File

@ -63,8 +63,8 @@ function TestTabActor(aConnection, aGlobal)
this.conn = aConnection;
this._global = aGlobal;
this._global.wrappedJSObject = aGlobal;
this._threadActor = new ThreadActor(this, this._global);
this.conn.addActor(this._threadActor);
this.threadActor = new ThreadActor(this, this._global);
this.conn.addActor(this.threadActor);
this._attached = false;
this._extraActors = {};
this.makeDebugger = makeDebugger.bind(null, {
@ -107,7 +107,7 @@ TestTabActor.prototype = {
onAttach: function(aRequest) {
this._attached = true;
let response = { type: "tabAttached", threadActor: this._threadActor.actorID };
let response = { type: "tabAttached", threadActor: this.threadActor.actorID };
this._appendExtraActors(response);
return response;

View File

@ -196,6 +196,7 @@ reason = bug 820380
[test_trace_actor-07.js]
[test_trace_actor-08.js]
[test_trace_actor-09.js]
[test_trace_actor-10.js]
[test_ignore_caught_exceptions.js]
[test_requestTypes.js]
reason = bug 937197