mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Merge fx-team to m-c. a=merge
This commit is contained in:
commit
23d518058d
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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":
|
||||
|
@ -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');"/>
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 () {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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' +
|
||||
' };' +
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
|
36
browser/base/content/test/social/share_activate.html
Normal file
36
browser/base/content/test/social/share_activate.html
Normal 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>
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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"
|
||||
});
|
||||
},
|
||||
|
||||
|
237
browser/components/loop/content/shared/js/websocket.js
Normal file
237
browser/components/loop/content/shared/js/websocket.js
Normal 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;
|
||||
})();
|
@ -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)
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
118
browser/components/loop/test/mochitest/loop_fxa.sjs
Normal file
118
browser/components/loop/test/mochitest/loop_fxa.sjs
Normal 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));
|
||||
}
|
@ -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 () {
|
||||
|
@ -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 () {},
|
||||
|
224
browser/components/loop/test/shared/websocket_test.js
Normal file
224
browser/components/loop/test/shared/websocket_test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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 -->
|
||||
|
@ -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() {
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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]
|
||||
|
86
browser/devtools/debugger/test/browser_dbg_tracing-07.js
Normal file
86
browser/devtools/debugger/test/browser_dbg_tracing-07.js
Normal 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;
|
||||
});
|
||||
|
@ -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…">
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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 */
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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 = {
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
167
toolkit/devtools/server/tests/unit/test_trace_actor-10.js
Normal file
167
toolkit/devtools/server/tests/unit/test_trace_actor-10.js
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user