Merge fx-team to m-c a=merge

This commit is contained in:
Wes Kocher 2015-01-16 16:45:53 -08:00
commit d8ef1b19ae
102 changed files with 2676 additions and 1931 deletions

View File

@ -1430,7 +1430,9 @@ pref("devtools.timeline.hiddenMarkers", "[]");
pref("devtools.performance.ui.show-timeline-memory", false);
// The default Profiler UI settings
pref("devtools.profiler.ui.flatten-tree-recursion", true);
pref("devtools.profiler.ui.show-platform-data", false);
pref("devtools.profiler.ui.show-idle-blocks", true);
// The default cache UI setting
pref("devtools.cache.disabled", false);
@ -1675,7 +1677,6 @@ pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data:
#endif
pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
pref("loop.rooms.enabled", true);
pref("loop.fxa_oauth.tokendata", "");
pref("loop.fxa_oauth.profile", "");
pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");

View File

@ -490,7 +490,7 @@ appUpdater.prototype =
return;
}
this.selectPanel("apply");
this.selectPanel("applyBillboard");
},
/**

View File

@ -399,9 +399,9 @@ var ctrlTab = {
suspendGUI: function ctrlTab_suspendGUI() {
document.removeEventListener("keyup", this, true);
Array.forEach(this.previews, function (preview) {
for (let preview of this.previews) {
this.updatePreview(preview, null);
}, this);
}
},
onKeyPress: function ctrlTab_onKeyPress(event) {

View File

@ -303,6 +303,8 @@ run-if = datareporting
skip-if = buildapp == 'mulet' || (os == "linux" && debug) || e10s # linux: bug 976544; e10s: bug 1071623
[browser_devices_get_user_media_about_urls.js]
skip-if = e10s # Bug 1071623
[browser_devices_get_user_media_in_frame.js]
skip-if = e10s # Bug 1071623
[browser_discovery.js]
[browser_double_close_tab.js]
skip-if = e10s

View File

@ -459,6 +459,50 @@ let gTests = [
}
},
{
desc: "getUserMedia audio+video: reloading the page removes all gUM UI",
run: function checkReloading() {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield checkSharingUI({video: true, audio: true});
yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices"));
info("reloading the web page");
let deferred = Promise.defer();
let browser = gBrowser.selectedBrowser;
browser.addEventListener("load", function onload() {
browser.removeEventListener("load", onload, true);
deferred.resolve();
}, true);
content.location.reload();
yield deferred.promise;
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (gObservedTopics["recording-device-events"] == 2) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
--gObservedTopics["recording-device-events"];
}
expectObserverCalled("recording-device-events");
expectObserverCalled("recording-window-ended");
expectNoObserverCalled();
yield checkNotSharing();
}
},
{
desc: "getUserMedia prompt: Always/Never Share",
run: function checkRememberCheckbox() {

View File

@ -0,0 +1,477 @@
/* 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/. */
const kObservedTopics = [
"getUserMedia:response:allow",
"getUserMedia:revoke",
"getUserMedia:response:deny",
"getUserMedia:request",
"recording-device-events",
"recording-window-ended"
];
const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
"@mozilla.org/mediaManagerService;1",
"nsIMediaManagerService");
var gObservedTopics = {};
function observer(aSubject, aTopic, aData) {
if (!(aTopic in gObservedTopics))
gObservedTopics[aTopic] = 1;
else
++gObservedTopics[aTopic];
}
function promiseObserverCalled(aTopic, aAction) {
let deferred = Promise.defer();
Services.obs.addObserver(function observer() {
ok(true, "got " + aTopic + " notification");
Services.obs.removeObserver(observer, aTopic);
if (kObservedTopics.indexOf(aTopic) != -1) {
if (!(aTopic in gObservedTopics))
gObservedTopics[aTopic] = -1;
else
--gObservedTopics[aTopic];
}
deferred.resolve();
}, aTopic, false);
if (aAction)
aAction();
return deferred.promise;
}
function expectObserverCalled(aTopic) {
is(gObservedTopics[aTopic], 1, "expected notification " + aTopic);
if (aTopic in gObservedTopics)
--gObservedTopics[aTopic];
}
function expectNoObserverCalled() {
for (let topic in gObservedTopics) {
if (gObservedTopics[topic])
is(gObservedTopics[topic], 0, topic + " notification unexpected");
}
gObservedTopics = {};
}
function promiseMessage(aMessage, aAction) {
let deferred = Promise.defer();
content.addEventListener("message", function messageListener(event) {
content.removeEventListener("message", messageListener);
is(event.data, aMessage, "received " + aMessage);
if (event.data == aMessage)
deferred.resolve();
else
deferred.reject();
});
if (aAction)
aAction();
return deferred.promise;
}
function promisePopupNotificationShown(aName, aAction) {
let deferred = Promise.defer();
PopupNotifications.panel.addEventListener("popupshown", function popupNotifShown() {
PopupNotifications.panel.removeEventListener("popupshown", popupNotifShown);
ok(!!PopupNotifications.getNotification(aName), aName + " notification shown");
ok(PopupNotifications.isPanelOpen, "notification panel open");
ok(!!PopupNotifications.panel.firstChild, "notification panel populated");
deferred.resolve();
});
if (aAction)
aAction();
return deferred.promise;
}
function promisePopupNotification(aName) {
let deferred = Promise.defer();
waitForCondition(() => PopupNotifications.getNotification(aName),
() => {
ok(!!PopupNotifications.getNotification(aName),
aName + " notification appeared");
deferred.resolve();
}, "timeout waiting for popup notification " + aName);
return deferred.promise;
}
function promiseNoPopupNotification(aName) {
let deferred = Promise.defer();
waitForCondition(() => !PopupNotifications.getNotification(aName),
() => {
ok(!PopupNotifications.getNotification(aName),
aName + " notification removed");
deferred.resolve();
}, "timeout waiting for popup notification " + aName + " to disappear");
return deferred.promise;
}
const kActionAlways = 1;
const kActionDeny = 2;
const kActionNever = 3;
function activateSecondaryAction(aAction) {
let notification = PopupNotifications.panel.firstChild;
notification.button.focus();
let popup = notification.menupopup;
popup.addEventListener("popupshown", function () {
popup.removeEventListener("popupshown", arguments.callee, false);
// Press 'down' as many time as needed to select the requested action.
while (aAction--)
EventUtils.synthesizeKey("VK_DOWN", {});
// Activate
EventUtils.synthesizeKey("VK_RETURN", {});
}, false);
// One down event to open the popup
EventUtils.synthesizeKey("VK_DOWN",
{ altKey: !navigator.platform.contains("Mac") });
}
registerCleanupFunction(function() {
gBrowser.removeCurrentTab();
kObservedTopics.forEach(topic => {
Services.obs.removeObserver(observer, topic);
});
Services.prefs.clearUserPref(PREF_PERMISSION_FAKE);
});
function getMediaCaptureState() {
let hasVideo = {};
let hasAudio = {};
MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio);
if (hasVideo.value && hasAudio.value)
return "CameraAndMicrophone";
if (hasVideo.value)
return "Camera";
if (hasAudio.value)
return "Microphone";
return "none";
}
function* closeStream(aGlobal, aAlreadyClosed) {
expectNoObserverCalled();
info("closing the stream");
aGlobal.closeStream();
if (!aAlreadyClosed)
yield promiseObserverCalled("recording-device-events");
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (!aAlreadyClosed)
expectObserverCalled("recording-window-ended");
yield* assertWebRTCIndicatorStatus(null);
}
function checkDeviceSelectors(aAudio, aVideo) {
let micSelector = document.getElementById("webRTC-selectMicrophone");
if (aAudio)
ok(!micSelector.hidden, "microphone selector visible");
else
ok(micSelector.hidden, "microphone selector hidden");
let cameraSelector = document.getElementById("webRTC-selectCamera");
if (aVideo)
ok(!cameraSelector.hidden, "camera selector visible");
else
ok(cameraSelector.hidden, "camera selector hidden");
}
function* checkSharingUI(aExpected) {
yield promisePopupNotification("webRTC-sharingDevices");
yield* assertWebRTCIndicatorStatus(aExpected);
}
function* checkNotSharing() {
is(getMediaCaptureState(), "none", "expected nothing to be shared");
ok(!PopupNotifications.getNotification("webRTC-sharingDevices"),
"no webRTC-sharingDevices popup notification");
yield* assertWebRTCIndicatorStatus(null);
}
function getFrameGlobal(aFrameId) {
return content.wrappedJSObject.document.getElementById(aFrameId).contentWindow;
}
const permissionError = "error: PermissionDeniedError: The user did not grant permission for the operation.";
let gTests = [
{
desc: "getUserMedia audio+video",
run: function checkAudioVideo() {
let global = getFrameGlobal("frame1");
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
global.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
"webRTC-shareDevices-notification-icon", "anchored to device icon");
checkDeviceSelectors(true, true);
is(PopupNotifications.panel.firstChild.getAttribute("popupid"),
"webRTC-shareDevices", "panel using devices icon");
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield checkSharingUI({audio: true, video: true});
yield closeStream(global);
}
},
{
desc: "getUserMedia audio+video: stop sharing",
run: function checkStopSharing() {
let global = getFrameGlobal("frame1");
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
global.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield checkSharingUI({video: true, audio: true});
yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices"));
activateSecondaryAction(kActionDeny);
yield promiseObserverCalled("recording-device-events");
expectObserverCalled("getUserMedia:revoke");
yield promiseNoPopupNotification("webRTC-sharingDevices");
expectObserverCalled("recording-window-ended");
if (gObservedTopics["recording-device-events"] == 1) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
gObservedTopics["recording-device-events"] = 0;
}
expectNoObserverCalled();
yield checkNotSharing();
// the stream is already closed, but this will do some cleanup anyway
yield closeStream(global, true);
}
},
{
desc: "getUserMedia audio+video: reloading the frame removes all sharing UI",
run: function checkReloading() {
let global = getFrameGlobal("frame1");
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
global.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield checkSharingUI({video: true, audio: true});
info("reloading the frame");
let deferred = Promise.defer();
let browser = gBrowser.selectedBrowser;
browser.addEventListener("load", function onload() {
browser.removeEventListener("load", onload, true);
deferred.resolve();
}, true);
global.location.reload();
yield deferred.promise;
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (gObservedTopics["recording-device-events"] == 2) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
--gObservedTopics["recording-device-events"];
}
expectObserverCalled("recording-device-events");
expectObserverCalled("recording-window-ended");
expectNoObserverCalled();
yield checkNotSharing();
}
},
{
desc: "getUserMedia audio+video: reloading the frame removes prompts",
run: function checkReloadingRemovesPrompts() {
let global = getFrameGlobal("frame1");
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
global.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
info("reloading the frame");
let deferred = Promise.defer();
let browser = gBrowser.selectedBrowser;
browser.addEventListener("load", function onload() {
browser.removeEventListener("load", onload, true);
deferred.resolve();
}, true);
global.location.reload();
yield deferred.promise;
yield promiseNoPopupNotification("webRTC-shareDevices");
expectObserverCalled("recording-window-ended");
expectNoObserverCalled();
yield checkNotSharing();
}
},
{
desc: "getUserMedia audio+video: reloading a frame updates the sharing UI",
run: function checkUpdateWhenReloading() {
// We'll share only the mic in the first frame, then share both in the
// second frame, then reload the second frame. After each step, we'll check
// the UI is in the correct state.
let g1 = getFrameGlobal("frame1"), g2 = getFrameGlobal("frame2");
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting microphone in the first frame");
g1.requestDevice(true, false);
});
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, false);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "Microphone", "microphone to be shared");
yield checkSharingUI({video: false, audio: true});
expectNoObserverCalled();
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting both devices in the second frame");
g2.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield checkSharingUI({video: true, audio: true});
expectNoObserverCalled();
info("reloading the second frame");
let deferred = Promise.defer();
let browser = gBrowser.selectedBrowser;
browser.addEventListener("load", function onload() {
browser.removeEventListener("load", onload, true);
deferred.resolve();
}, true);
g2.location.reload();
yield deferred.promise;
yield checkSharingUI({video: false, audio: true});
expectObserverCalled("recording-window-ended");
if (gObservedTopics["recording-device-events"] == 2) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
--gObservedTopics["recording-device-events"];
}
expectObserverCalled("recording-device-events");
expectNoObserverCalled();
yield closeStream(g1);
yield promiseNoPopupNotification("webRTC-sharingDevices");
expectNoObserverCalled();
yield checkNotSharing();
}
}
];
function test() {
waitForExplicitFinish();
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
tab.linkedBrowser.addEventListener("load", function onload() {
tab.linkedBrowser.removeEventListener("load", onload, true);
kObservedTopics.forEach(topic => {
Services.obs.addObserver(observer, topic, false);
});
Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true);
is(PopupNotifications._currentNotifications.length, 0,
"should start the test without any prior popup notification");
Task.spawn(function () {
for (let test of gTests) {
info(test.desc);
yield test.run();
// Cleanup before the next test
expectNoObserverCalled();
}
}).then(finish, ex => {
ok(false, "Unexpected Exception: " + ex);
finish();
});
}, true);
let rootDir = getRootDirectory(gTestPath);
rootDir = rootDir.replace("chrome://mochitests/content/",
"https://example.com/");
let url = rootDir + "get_user_media.html";
content.location = 'data:text/html,<iframe id="frame1" src="' + url + '"></iframe><iframe id="frame2" src="' + url + '"></iframe>'
}

View File

@ -334,14 +334,16 @@
Cu.reportError(ex);
}
function loadCurrent() {
let loadCurrent = () => {
openUILinkIn(url, "current", {
allowThirdPartyFixup: true,
disallowInheritPrincipal: !mayInheritPrincipal,
allowPinnedTabHostChange: true,
postData: postData
});
}
// Ensure the start of the URL is visible for UX reasons:
this.selectionStart = this.selectionEnd = 0;
};
// Focus the content area before triggering loads, since if the load
// occurs in a new tab, we want focus to be restored to the content
@ -1149,7 +1151,8 @@
let header = document.getAnonymousElementByAttribute(this, "anonid",
"search-panel-one-offs-header")
header.collapsed = list.collapsed = !engines.length;
// header is a xul:deck so collapsed doesn't work on it, see bug 589569.
header.hidden = list.collapsed = !engines.length;
// 49px is the min-width of each search engine button,
// adapt this const when changing the css.

View File

@ -8,3 +8,7 @@ skip-if = os == "linux" # Bug 949434
[browser_overflow_anchor.js]
skip-if = os == "linux" # Bug 952422
[browser_confirm_unblock_download.js]
[browser_iframe_gone_mid_download.js]
skip-if = e10s

View File

@ -0,0 +1,62 @@
const SAVE_PER_SITE_PREF = "browser.download.lastDir.savePerSite";
function test_deleted_iframe(perSitePref, windowOptions={}) {
return function*() {
Services.prefs.setBoolPref(SAVE_PER_SITE_PREF, perSitePref);
let {DownloadLastDir} = Cu.import("resource://gre/modules/DownloadLastDir.jsm", {});
let win = yield promiseOpenAndLoadWindow(windowOptions);
let tab = win.gBrowser.addTab();
yield promiseTabLoadEvent(tab, "about:mozilla");
let doc = tab.linkedBrowser.contentDocument;
let iframe = doc.createElement("iframe");
doc.body.appendChild(iframe);
ok(iframe.contentWindow, "iframe should have a window");
let gDownloadLastDir = new DownloadLastDir(iframe.contentWindow);
let cw = iframe.contentWindow;
let promiseIframeWindowGone = new Promise((resolve, reject) => {
Services.obs.addObserver(function obs(subject, topic) {
if (subject == cw) {
Services.obs.removeObserver(obs, topic);
resolve();
}
}, "dom-window-destroyed", false);
});
iframe.remove();
yield promiseIframeWindowGone;
cw = null;
ok(!iframe.contentWindow, "Managed to destroy iframe");
let someDir = "blah";
try {
someDir = yield new Promise((resolve, reject) => {
gDownloadLastDir.getFileAsync("http://www.mozilla.org/", function(dir) {
resolve(dir);
});
});
} catch (ex) {
ok(false, "Got an exception trying to get the directory where things should be saved.");
Cu.reportError(ex);
}
// NB: someDir can legitimately be null here when set, hence the 'blah' workaround:
isnot(someDir, "blah", "Should get a file even after the window was destroyed.");
try {
gDownloadLastDir.setFile("http://www.mozilla.org/", null);
} catch (ex) {
ok(false, "Got an exception trying to set the directory where things should be saved.");
Cu.reportError(ex);
}
yield promiseWindowClosed(win);
Services.prefs.clearUserPref(SAVE_PER_SITE_PREF);
};
}
add_task(test_deleted_iframe(false));
add_task(test_deleted_iframe(false));
add_task(test_deleted_iframe(true, {private: true}));
add_task(test_deleted_iframe(true, {private: true}));

View File

@ -31,6 +31,76 @@ registerCleanupFunction(function () {
////////////////////////////////////////////////////////////////////////////////
//// Asynchronous support subroutines
function promiseOpenAndLoadWindow(aOptions)
{
return new Promise((resolve, reject) => {
let win = OpenBrowserWindow(aOptions);
win.addEventListener("load", function onLoad() {
win.removeEventListener("load", onLoad);
resolve(win);
});
});
}
/**
* Waits for a load (or custom) event to finish in a given tab. If provided
* load an uri into the tab.
*
* @param tab
* The tab to load into.
* @param [optional] url
* The url to load, or the current url.
* @param [optional] event
* The load event type to wait for. Defaults to "load".
* @return {Promise} resolved when the event is handled.
* @resolves to the received event
* @rejects if a valid load event is not received within a meaningful interval
*/
function promiseTabLoadEvent(tab, url, eventType="load")
{
let deferred = Promise.defer();
info("Wait tab event: " + eventType);
function handle(event) {
if (event.originalTarget != tab.linkedBrowser.contentDocument ||
event.target.location.href == "about:blank" ||
(url && event.target.location.href != url)) {
info("Skipping spurious '" + eventType + "'' event" +
" for " + event.target.location.href);
return;
}
// Remove reference to tab from the cleanup function:
realCleanup = () => {};
tab.linkedBrowser.removeEventListener(eventType, handle, true);
info("Tab event received: " + eventType);
deferred.resolve(event);
}
// Juggle a bit to avoid leaks:
let realCleanup = () => tab.linkedBrowser.removeEventListener(eventType, handle, true);
registerCleanupFunction(() => realCleanup());
tab.linkedBrowser.addEventListener(eventType, handle, true, true);
if (url)
tab.linkedBrowser.loadURI(url);
return deferred.promise;
}
function promiseWindowClosed(win)
{
let promise = new Promise((resolve, reject) => {
Services.obs.addObserver(function obs(subject, topic) {
if (subject == win) {
Services.obs.removeObserver(obs, topic);
resolve();
}
}, "domwindowclosed", false);
});
win.close();
return promise;
}
function promiseFocus()
{
let deferred = Promise.defer();

View File

@ -416,26 +416,6 @@ function injectLoopAPI(targetWindow) {
}
},
/**
* Used to note a call url expiry time. If the time is later than the current
* latest expiry time, then the stored expiry time is increased. For times
* sooner, this function is a no-op; this ensures we always have the latest
* expiry time for a url.
*
* This is used to determine whether or not we should be registering with the
* push server on start.
*
* @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
* of the url.
*/
noteCallUrlExpiry: {
enumerable: true,
writable: true,
value: function(expiryTimeSeconds) {
MozLoopService.noteCallUrlExpiry(expiryTimeSeconds);
}
},
/**
* Set any preference under "loop."
*

View File

@ -1243,22 +1243,6 @@ this.MozLoopService = {
return MozLoopServiceInternal.promiseRegisteredWithServers(sessionType);
},
/**
* Used to note a call url expiry time. If the time is later than the current
* latest expiry time, then the stored expiry time is increased. For times
* sooner, this function is a no-op; this ensures we always have the latest
* expiry time for a url.
*
* This is used to determine whether or not we should be registering with the
* push server on start.
*
* @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
* of the url.
*/
noteCallUrlExpiry: function(expiryTimeSeconds) {
MozLoopServiceInternal.expiryTimeSeconds = expiryTimeSeconds;
},
/**
* Returns the strings for the specified element. Designed for use with l10n.js.
*

View File

@ -9,12 +9,6 @@ var loop = loop || {};
loop.Client = (function($) {
"use strict";
// The expected properties to be returned from the POST /call-url/ request.
var expectedCallUrlProperties = ["callUrl", "expiresAt"];
// The expected properties to be returned from the GET /calls request.
var expectedCallProperties = ["calls"];
// THe expected properties to be returned from the POST /calls request.
var expectedPostCallProperties = [
"apiKey", "callId", "progressURL",
@ -81,56 +75,6 @@ loop.Client = (function($) {
cb(error);
},
/**
* Requests a call URL from the Loop server. It will note the
* expiry time for the url with the mozLoop api. It will select the
* appropriate hawk session to use based on whether or not the user
* is currently logged into a Firefox account profile.
*
* Callback parameters:
* - err null on successful request, non-null otherwise.
* - callUrlData an object of the obtained call url data if successful:
* -- callUrl: The url of the call
* -- expiresAt: The amount of hours until expiry of the url
*
* @param {String} simplepushUrl a registered Simple Push URL
* @param {string} nickname the nickname of the future caller
* @param {Function} cb Callback(err, callUrlData)
*/
requestCallUrl: function(nickname, cb) {
var sessionType;
if (this.mozLoop.userProfile) {
sessionType = this.mozLoop.LOOP_SESSION_TYPE.FXA;
} else {
sessionType = this.mozLoop.LOOP_SESSION_TYPE.GUEST;
}
this.mozLoop.hawkRequest(sessionType, "/call-url/", "POST",
{callerId: nickname},
function (error, responseText) {
if (error) {
this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", false);
this._failureHandler(cb, error);
return;
}
try {
var urlData = JSON.parse(responseText);
// This throws if the data is invalid, in which case only the failure
// telemetry will be recorded.
var returnData = this._validate(urlData, expectedCallUrlProperties);
this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", true);
cb(null, returnData);
} catch (err) {
this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", false);
console.log("Error requesting call info", err);
cb(err);
}
}.bind(this));
},
/**
* Block call URL based on the token identifier
*
@ -203,20 +147,6 @@ loop.Client = (function($) {
}.bind(this)
);
},
/**
* Adds a value to a telemetry histogram, ignoring errors.
*
* @param {string} histogramId Name of the telemetry histogram to update.
* @param {integer} value Value to add to the histogram.
*/
_telemetryAdd: function(histogramId, value) {
try {
this.mozLoop.telemetryAdd(histogramId, value);
} catch (err) {
console.error("Error recording telemetry", err);
}
},
};
return Client;

View File

@ -263,6 +263,11 @@ loop.contacts = (function(_, mozL10n) {
loop.shared.mixins.WindowCloseMixin
],
propTypes: {
notifications: React.PropTypes.instanceOf(
loop.shared.models.NotificationCollection).isRequired
},
/**
* Contacts collection object
*/
@ -389,10 +394,14 @@ loop.contacts = (function(_, mozL10n) {
service: "google"
}, (err, stats) => {
this.setState({ importBusy: false });
// TODO: bug 1076764 - proper error and success reporting.
if (err) {
throw err;
console.error("Contact import error", err);
this.props.notifications.errorL10n("import_contacts_failure_message");
return;
}
this.props.notifications.successL10n("import_contacts_success_message", {
total: stats.total
});
});
},

View File

@ -263,6 +263,11 @@ loop.contacts = (function(_, mozL10n) {
loop.shared.mixins.WindowCloseMixin
],
propTypes: {
notifications: React.PropTypes.instanceOf(
loop.shared.models.NotificationCollection).isRequired
},
/**
* Contacts collection object
*/
@ -389,10 +394,14 @@ loop.contacts = (function(_, mozL10n) {
service: "google"
}, (err, stats) => {
this.setState({ importBusy: false });
// TODO: bug 1076764 - proper error and success reporting.
if (err) {
throw err;
console.error("Contact import error", err);
this.props.notifications.errorL10n("import_contacts_failure_message");
return;
}
this.props.notifications.successL10n("import_contacts_success_message", {
total: stats.total
});
});
},

View File

@ -835,6 +835,10 @@ loop.conversationViews = (function(mozL10n) {
});
var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
mixins: [
sharedMixins.MediaSetupMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
video: React.PropTypes.object,
@ -849,75 +853,18 @@ loop.conversationViews = (function(mozL10n) {
},
componentDidMount: function() {
/**
* OT inserts inline styles into the markup. Using a listener for
* resize events helps us trigger a full width/height on the element
* so that they update to the correct dimensions.
* XXX: this should be factored as a mixin.
*/
window.addEventListener('orientationchange', this.updateVideoContainer);
window.addEventListener('resize', this.updateVideoContainer);
// The SDK needs to know about the configuration and the elements to use
// for display. So the best way seems to pass the information here - ideally
// the sdk wouldn't need to know this, but we can't change that.
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this._getPublisherConfig(),
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: this.props.video.enabled
}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
}));
},
componentWillUnmount: function() {
window.removeEventListener('orientationchange', this.updateVideoContainer);
window.removeEventListener('resize', this.updateVideoContainer);
},
/**
* Returns either the required DOMNode
*
* @param {String} className The name of the class to get the element for.
*/
_getElement: function(className) {
return this.getDOMNode().querySelector(className);
},
/**
* Returns the required configuration for publishing video on the sdk.
*/
_getPublisherConfig: function() {
// height set to 100%" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
return {
insertMode: "append",
width: "100%",
height: "100%",
publishVideo: this.props.video.enabled,
style: {
audioLevelDisplayMode: "off",
bugDisplayMode: "off",
buttonDisplayMode: "off",
nameDisplayMode: "off",
videoDisabledDisplayMode: "off"
}
};
},
/**
* Used to update the video container whenever the orientation or size of the
* display area changes.
*/
updateVideoContainer: function() {
var localStreamParent = this._getElement('.local .OT_publisher');
var remoteStreamParent = this._getElement('.remote .OT_subscriber');
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
},
/**
* Hangs up the call.
*/

View File

@ -835,6 +835,10 @@ loop.conversationViews = (function(mozL10n) {
});
var OngoingConversationView = React.createClass({
mixins: [
sharedMixins.MediaSetupMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
video: React.PropTypes.object,
@ -849,75 +853,18 @@ loop.conversationViews = (function(mozL10n) {
},
componentDidMount: function() {
/**
* OT inserts inline styles into the markup. Using a listener for
* resize events helps us trigger a full width/height on the element
* so that they update to the correct dimensions.
* XXX: this should be factored as a mixin.
*/
window.addEventListener('orientationchange', this.updateVideoContainer);
window.addEventListener('resize', this.updateVideoContainer);
// The SDK needs to know about the configuration and the elements to use
// for display. So the best way seems to pass the information here - ideally
// the sdk wouldn't need to know this, but we can't change that.
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this._getPublisherConfig(),
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: this.props.video.enabled
}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
}));
},
componentWillUnmount: function() {
window.removeEventListener('orientationchange', this.updateVideoContainer);
window.removeEventListener('resize', this.updateVideoContainer);
},
/**
* Returns either the required DOMNode
*
* @param {String} className The name of the class to get the element for.
*/
_getElement: function(className) {
return this.getDOMNode().querySelector(className);
},
/**
* Returns the required configuration for publishing video on the sdk.
*/
_getPublisherConfig: function() {
// height set to 100%" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
return {
insertMode: "append",
width: "100%",
height: "100%",
publishVideo: this.props.video.enabled,
style: {
audioLevelDisplayMode: "off",
bugDisplayMode: "off",
buttonDisplayMode: "off",
nameDisplayMode: "off",
videoDisabledDisplayMode: "off"
}
};
},
/**
* Used to update the video container whenever the orientation or size of the
* display area changes.
*/
updateVideoContainer: function() {
var localStreamParent = this._getElement('.local .OT_publisher');
var remoteStreamParent = this._getElement('.remote .OT_subscriber');
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
},
/**
* Hangs up the call.
*/

View File

@ -39,9 +39,7 @@ loop.panel = (function(_, mozL10n) {
// When we don't need to rely on the pref, this can move back to
// getDefaultProps (bug 1100258).
return {
selectedTab: this.props.selectedTab ||
(navigator.mozLoop.getLoopPref("rooms.enabled") ?
"rooms" : "call")
selectedTab: this.props.selectedTab || "rooms"
};
},
@ -358,157 +356,6 @@ loop.panel = (function(_, mozL10n) {
}
});
/**
* Call url result view.
*/
var CallUrlResult = React.createClass({displayName: "CallUrlResult",
mixins: [sharedMixins.DocumentVisibilityMixin],
propTypes: {
callUrl: React.PropTypes.string,
callUrlExpiry: React.PropTypes.number,
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
pending: false,
copied: false,
callUrl: this.props.callUrl || "",
callUrlExpiry: 0
};
},
/**
* Provided by DocumentVisibilityMixin. Schedules retrieval of a new call
* URL everytime the panel is reopened.
*/
onDocumentVisible: function() {
this._fetchCallUrl();
},
componentDidMount: function() {
// If we've already got a callURL, don't bother requesting a new one.
// As of this writing, only used for visual testing in the UI showcase.
if (this.state.callUrl.length) {
return;
}
this._fetchCallUrl();
},
/**
* Fetches a call URL.
*/
_fetchCallUrl: function() {
this.setState({pending: true});
// XXX This is an empty string as a conversation identifier. Bug 1015938 implements
// a user-set string.
this.props.client.requestCallUrl("",
this._onCallUrlReceived);
},
_onCallUrlReceived: function(err, callUrlData) {
if (err) {
if (err.code != 401) {
// 401 errors are already handled in hawkRequest and show an error
// message about the session.
this.props.notifications.errorL10n("unable_retrieve_url");
}
this.setState(this.getInitialState());
} else {
try {
var callUrl = new window.URL(callUrlData.callUrl);
// XXX the current server vers does not implement the callToken field
// but it exists in the API. This workaround should be removed in the future
var token = callUrlData.callToken ||
callUrl.pathname.split('/').pop();
// Now that a new URL is available, indicate it has not been shared.
this.linkExfiltrated = false;
this.setState({pending: false, copied: false,
callUrl: callUrl.href,
callUrlExpiry: callUrlData.expiresAt});
} catch(e) {
console.log(e);
this.props.notifications.errorL10n("unable_retrieve_url");
this.setState(this.getInitialState());
}
}
},
handleEmailButtonClick: function(event) {
this.handleLinkExfiltration(event);
sharedUtils.composeCallUrlEmail(this.state.callUrl);
},
handleCopyButtonClick: function(event) {
this.handleLinkExfiltration(event);
// XXX the mozLoop object should be passed as a prop, to ease testing and
// using a fake implementation in UI components showcase.
navigator.mozLoop.copyString(this.state.callUrl);
this.setState({copied: true});
},
linkExfiltrated: false,
handleLinkExfiltration: function(event) {
// Update the count of shared URLs only once per generated URL.
if (!this.linkExfiltrated) {
this.linkExfiltrated = true;
try {
navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true);
} catch (err) {
console.error("Error recording telemetry", err);
}
}
// Note URL expiration every time it is shared.
if (this.state.callUrlExpiry) {
navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
}
},
render: function() {
// XXX setting elem value from a state (in the callUrl input)
// makes it immutable ie read only but that is fine in our case.
// readOnly attr will suppress a warning regarding this issue
// from the react lib.
var cx = React.addons.classSet;
return (
React.createElement("div", {className: "generate-url"},
React.createElement("header", {id: "share-link-header"}, mozL10n.get("share_link_header_text")),
React.createElement("div", {className: "generate-url-stack"},
React.createElement("input", {type: "url", value: this.state.callUrl, readOnly: "true",
onCopy: this.handleLinkExfiltration,
className: cx({"generate-url-input": true,
pending: this.state.pending,
// Used in functional testing, signals that
// call url was received from loop server
callUrl: !this.state.pending})}),
React.createElement("div", {className: cx({"generate-url-spinner": true,
spinner: true,
busy: this.state.pending})})
),
React.createElement(ButtonGroup, {additionalClass: "url-actions"},
React.createElement(Button, {additionalClass: "button-email",
disabled: !this.state.callUrl,
onClick: this.handleEmailButtonClick,
caption: mozL10n.get("share_button")}),
React.createElement(Button, {additionalClass: "button-copy",
disabled: !this.state.callUrl,
onClick: this.handleCopyButtonClick,
caption: this.state.copied ? mozL10n.get("copied_url_button") :
mozL10n.get("copy_url_button")})
)
)
);
}
});
/**
* FxA sign in/up link component.
*/
@ -820,9 +667,7 @@ loop.panel = (function(_, mozL10n) {
var PanelView = React.createClass({displayName: "PanelView",
propTypes: {
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired,
// Mostly used for UI components showcase and unit tests
callUrl: React.PropTypes.string,
userProfile: React.PropTypes.object,
// Used only for unit tests.
showTabButtons: React.PropTypes.bool,
@ -869,17 +714,13 @@ loop.panel = (function(_, mozL10n) {
}
},
_roomsEnabled: function() {
return this.props.mozLoop.getLoopPref("rooms.enabled");
},
_onStatusChanged: function() {
var profile = this.props.mozLoop.userProfile;
var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
var newUid = profile ? profile.uid : null;
if (currUid != newUid) {
// On profile change (login, logout), switch back to the default tab.
this.selectTab(this._roomsEnabled() ? "rooms" : "call");
this.selectTab("rooms");
this.setState({userProfile: profile});
}
this.updateServiceErrors();
@ -902,34 +743,6 @@ loop.panel = (function(_, mozL10n) {
}
},
/**
* The rooms feature is hidden by default for now. Once it gets mainstream,
* this method can be simplified.
*/
_renderRoomsOrCallTab: function() {
if (!this._roomsEnabled()) {
return (
React.createElement(Tab, {name: "call"},
React.createElement("div", {className: "content-area"},
React.createElement(CallUrlResult, {client: this.props.client,
notifications: this.props.notifications,
callUrl: this.props.callUrl}),
React.createElement(ToSView, null)
)
)
);
}
return (
React.createElement(Tab, {name: "rooms"},
React.createElement(RoomList, {dispatcher: this.props.dispatcher,
store: this.props.roomStore,
userDisplayName: this._getUserDisplayName()}),
React.createElement(ToSView, null)
)
);
},
startForm: function(name, contact) {
this.refs[name].initForm(contact);
this.selectTab(name);
@ -986,10 +799,16 @@ loop.panel = (function(_, mozL10n) {
clearOnDocumentHidden: true}),
React.createElement(TabView, {ref: "tabView", selectedTab: this.props.selectedTab,
buttonsHidden: hideButtons},
this._renderRoomsOrCallTab(),
React.createElement(Tab, {name: "rooms"},
React.createElement(RoomList, {dispatcher: this.props.dispatcher,
store: this.props.roomStore,
userDisplayName: this._getUserDisplayName()}),
React.createElement(ToSView, null)
),
React.createElement(Tab, {name: "contacts"},
React.createElement(ContactsList, {selectTab: this.selectTab,
startForm: this.startForm})
startForm: this.startForm,
notifications: this.props.notifications})
),
React.createElement(Tab, {name: "contacts_add", hidden: true},
React.createElement(ContactDetailsForm, {ref: "contacts_add", mode: "add",
@ -1028,7 +847,6 @@ loop.panel = (function(_, mozL10n) {
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(navigator.mozLoop);
var client = new loop.Client();
var notifications = new sharedModels.NotificationCollection();
var dispatcher = new loop.Dispatcher();
var roomStore = new loop.store.RoomStore(dispatcher, {
@ -1037,7 +855,6 @@ loop.panel = (function(_, mozL10n) {
});
React.render(React.createElement(PanelView, {
client: client,
notifications: notifications,
roomStore: roomStore,
mozLoop: navigator.mozLoop,
@ -1056,7 +873,6 @@ loop.panel = (function(_, mozL10n) {
init: init,
AuthLink: AuthLink,
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
GettingStartedView: GettingStartedView,
PanelView: PanelView,
RoomEntry: RoomEntry,

View File

@ -39,9 +39,7 @@ loop.panel = (function(_, mozL10n) {
// When we don't need to rely on the pref, this can move back to
// getDefaultProps (bug 1100258).
return {
selectedTab: this.props.selectedTab ||
(navigator.mozLoop.getLoopPref("rooms.enabled") ?
"rooms" : "call")
selectedTab: this.props.selectedTab || "rooms"
};
},
@ -358,157 +356,6 @@ loop.panel = (function(_, mozL10n) {
}
});
/**
* Call url result view.
*/
var CallUrlResult = React.createClass({
mixins: [sharedMixins.DocumentVisibilityMixin],
propTypes: {
callUrl: React.PropTypes.string,
callUrlExpiry: React.PropTypes.number,
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
pending: false,
copied: false,
callUrl: this.props.callUrl || "",
callUrlExpiry: 0
};
},
/**
* Provided by DocumentVisibilityMixin. Schedules retrieval of a new call
* URL everytime the panel is reopened.
*/
onDocumentVisible: function() {
this._fetchCallUrl();
},
componentDidMount: function() {
// If we've already got a callURL, don't bother requesting a new one.
// As of this writing, only used for visual testing in the UI showcase.
if (this.state.callUrl.length) {
return;
}
this._fetchCallUrl();
},
/**
* Fetches a call URL.
*/
_fetchCallUrl: function() {
this.setState({pending: true});
// XXX This is an empty string as a conversation identifier. Bug 1015938 implements
// a user-set string.
this.props.client.requestCallUrl("",
this._onCallUrlReceived);
},
_onCallUrlReceived: function(err, callUrlData) {
if (err) {
if (err.code != 401) {
// 401 errors are already handled in hawkRequest and show an error
// message about the session.
this.props.notifications.errorL10n("unable_retrieve_url");
}
this.setState(this.getInitialState());
} else {
try {
var callUrl = new window.URL(callUrlData.callUrl);
// XXX the current server vers does not implement the callToken field
// but it exists in the API. This workaround should be removed in the future
var token = callUrlData.callToken ||
callUrl.pathname.split('/').pop();
// Now that a new URL is available, indicate it has not been shared.
this.linkExfiltrated = false;
this.setState({pending: false, copied: false,
callUrl: callUrl.href,
callUrlExpiry: callUrlData.expiresAt});
} catch(e) {
console.log(e);
this.props.notifications.errorL10n("unable_retrieve_url");
this.setState(this.getInitialState());
}
}
},
handleEmailButtonClick: function(event) {
this.handleLinkExfiltration(event);
sharedUtils.composeCallUrlEmail(this.state.callUrl);
},
handleCopyButtonClick: function(event) {
this.handleLinkExfiltration(event);
// XXX the mozLoop object should be passed as a prop, to ease testing and
// using a fake implementation in UI components showcase.
navigator.mozLoop.copyString(this.state.callUrl);
this.setState({copied: true});
},
linkExfiltrated: false,
handleLinkExfiltration: function(event) {
// Update the count of shared URLs only once per generated URL.
if (!this.linkExfiltrated) {
this.linkExfiltrated = true;
try {
navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true);
} catch (err) {
console.error("Error recording telemetry", err);
}
}
// Note URL expiration every time it is shared.
if (this.state.callUrlExpiry) {
navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
}
},
render: function() {
// XXX setting elem value from a state (in the callUrl input)
// makes it immutable ie read only but that is fine in our case.
// readOnly attr will suppress a warning regarding this issue
// from the react lib.
var cx = React.addons.classSet;
return (
<div className="generate-url">
<header id="share-link-header">{mozL10n.get("share_link_header_text")}</header>
<div className="generate-url-stack">
<input type="url" value={this.state.callUrl} readOnly="true"
onCopy={this.handleLinkExfiltration}
className={cx({"generate-url-input": true,
pending: this.state.pending,
// Used in functional testing, signals that
// call url was received from loop server
callUrl: !this.state.pending})} />
<div className={cx({"generate-url-spinner": true,
spinner: true,
busy: this.state.pending})} />
</div>
<ButtonGroup additionalClass="url-actions">
<Button additionalClass="button-email"
disabled={!this.state.callUrl}
onClick={this.handleEmailButtonClick}
caption={mozL10n.get("share_button")} />
<Button additionalClass="button-copy"
disabled={!this.state.callUrl}
onClick={this.handleCopyButtonClick}
caption={this.state.copied ? mozL10n.get("copied_url_button") :
mozL10n.get("copy_url_button")} />
</ButtonGroup>
</div>
);
}
});
/**
* FxA sign in/up link component.
*/
@ -820,9 +667,7 @@ loop.panel = (function(_, mozL10n) {
var PanelView = React.createClass({
propTypes: {
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired,
// Mostly used for UI components showcase and unit tests
callUrl: React.PropTypes.string,
userProfile: React.PropTypes.object,
// Used only for unit tests.
showTabButtons: React.PropTypes.bool,
@ -869,17 +714,13 @@ loop.panel = (function(_, mozL10n) {
}
},
_roomsEnabled: function() {
return this.props.mozLoop.getLoopPref("rooms.enabled");
},
_onStatusChanged: function() {
var profile = this.props.mozLoop.userProfile;
var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
var newUid = profile ? profile.uid : null;
if (currUid != newUid) {
// On profile change (login, logout), switch back to the default tab.
this.selectTab(this._roomsEnabled() ? "rooms" : "call");
this.selectTab("rooms");
this.setState({userProfile: profile});
}
this.updateServiceErrors();
@ -902,34 +743,6 @@ loop.panel = (function(_, mozL10n) {
}
},
/**
* The rooms feature is hidden by default for now. Once it gets mainstream,
* this method can be simplified.
*/
_renderRoomsOrCallTab: function() {
if (!this._roomsEnabled()) {
return (
<Tab name="call">
<div className="content-area">
<CallUrlResult client={this.props.client}
notifications={this.props.notifications}
callUrl={this.props.callUrl} />
<ToSView />
</div>
</Tab>
);
}
return (
<Tab name="rooms">
<RoomList dispatcher={this.props.dispatcher}
store={this.props.roomStore}
userDisplayName={this._getUserDisplayName()}/>
<ToSView />
</Tab>
);
},
startForm: function(name, contact) {
this.refs[name].initForm(contact);
this.selectTab(name);
@ -986,10 +799,16 @@ loop.panel = (function(_, mozL10n) {
clearOnDocumentHidden={true} />
<TabView ref="tabView" selectedTab={this.props.selectedTab}
buttonsHidden={hideButtons}>
{this._renderRoomsOrCallTab()}
<Tab name="rooms">
<RoomList dispatcher={this.props.dispatcher}
store={this.props.roomStore}
userDisplayName={this._getUserDisplayName()}/>
<ToSView />
</Tab>
<Tab name="contacts">
<ContactsList selectTab={this.selectTab}
startForm={this.startForm} />
startForm={this.startForm}
notifications={this.props.notifications} />
</Tab>
<Tab name="contacts_add" hidden={true}>
<ContactDetailsForm ref="contacts_add" mode="add"
@ -1028,7 +847,6 @@ loop.panel = (function(_, mozL10n) {
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(navigator.mozLoop);
var client = new loop.Client();
var notifications = new sharedModels.NotificationCollection();
var dispatcher = new loop.Dispatcher();
var roomStore = new loop.store.RoomStore(dispatcher, {
@ -1037,7 +855,6 @@ loop.panel = (function(_, mozL10n) {
});
React.render(<PanelView
client={client}
notifications={notifications}
roomStore={roomStore}
mozLoop={navigator.mozLoop}
@ -1056,7 +873,6 @@ loop.panel = (function(_, mozL10n) {
init: init,
AuthLink: AuthLink,
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
GettingStartedView: GettingStartedView,
PanelView: PanelView,
RoomEntry: RoomEntry,

View File

@ -164,6 +164,7 @@ loop.roomViews = (function(mozL10n) {
mixins: [
ActiveRoomStoreMixin,
sharedMixins.DocumentTitleMixin,
sharedMixins.MediaSetupMixin,
sharedMixins.RoomsAudioMixin
],
@ -183,17 +184,6 @@ loop.roomViews = (function(mozL10n) {
return null;
},
componentDidMount: function() {
/**
* OT inserts inline styles into the markup. Using a listener for
* resize events helps us trigger a full width/height on the element
* so that they update to the correct dimensions.
* XXX: this should be factored as a mixin.
*/
window.addEventListener('orientationchange', this.updateVideoContainer);
window.addEventListener('resize', this.updateVideoContainer);
},
componentWillUpdate: function(nextProps, nextState) {
// The SDK needs to know about the configuration and the elements to use
// for display. So the best way seems to pass the information here - ideally
@ -201,55 +191,15 @@ loop.roomViews = (function(mozL10n) {
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this._getPublisherConfig(),
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: !this.state.videoMuted
}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
}));
}
},
_getPublisherConfig: function() {
// height set to 100%" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
return {
insertMode: "append",
width: "100%",
height: "100%",
publishVideo: !this.state.videoMuted,
style: {
audioLevelDisplayMode: "off",
bugDisplayMode: "off",
buttonDisplayMode: "off",
nameDisplayMode: "off",
videoDisabledDisplayMode: "off"
}
};
},
/**
* Used to update the video container whenever the orientation or size of the
* display area changes.
*/
updateVideoContainer: function() {
var localStreamParent = this._getElement('.local .OT_publisher');
var remoteStreamParent = this._getElement('.remote .OT_subscriber');
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
},
/**
* Returns either the required DOMNode
*
* @param {String} className The name of the class to get the element for.
*/
_getElement: function(className) {
return this.getDOMNode().querySelector(className);
},
/**
* User clicked on the "Leave" button.
*/

View File

@ -164,6 +164,7 @@ loop.roomViews = (function(mozL10n) {
mixins: [
ActiveRoomStoreMixin,
sharedMixins.DocumentTitleMixin,
sharedMixins.MediaSetupMixin,
sharedMixins.RoomsAudioMixin
],
@ -183,17 +184,6 @@ loop.roomViews = (function(mozL10n) {
return null;
},
componentDidMount: function() {
/**
* OT inserts inline styles into the markup. Using a listener for
* resize events helps us trigger a full width/height on the element
* so that they update to the correct dimensions.
* XXX: this should be factored as a mixin.
*/
window.addEventListener('orientationchange', this.updateVideoContainer);
window.addEventListener('resize', this.updateVideoContainer);
},
componentWillUpdate: function(nextProps, nextState) {
// The SDK needs to know about the configuration and the elements to use
// for display. So the best way seems to pass the information here - ideally
@ -201,55 +191,15 @@ loop.roomViews = (function(mozL10n) {
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this._getPublisherConfig(),
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: !this.state.videoMuted
}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
}));
}
},
_getPublisherConfig: function() {
// height set to 100%" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
return {
insertMode: "append",
width: "100%",
height: "100%",
publishVideo: !this.state.videoMuted,
style: {
audioLevelDisplayMode: "off",
bugDisplayMode: "off",
buttonDisplayMode: "off",
nameDisplayMode: "off",
videoDisabledDisplayMode: "off"
}
};
},
/**
* Used to update the video container whenever the orientation or size of the
* display area changes.
*/
updateVideoContainer: function() {
var localStreamParent = this._getElement('.local .OT_publisher');
var remoteStreamParent = this._getElement('.remote .OT_subscriber');
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
},
/**
* Returns either the required DOMNode
*
* @param {String} className The name of the class to get the element for.
*/
_getElement: function(className) {
return this.getDOMNode().querySelector(className);
},
/**
* User clicked on the "Leave" button.
*/

View File

@ -32,7 +32,6 @@
<script type="text/javascript" src="loop/shared/js/roomStates.js"></script>
<script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript;version=1.8" src="loop/js/contacts.js"></script>
<script type="text/javascript" src="loop/js/panel.js"></script>
</body>

View File

@ -263,6 +263,12 @@ p {
border: 1px solid #fbeed5;
}
.alert-success {
background: #5BC0A4;
border: 1px solid #5BC0A4;
color: #fff;
}
.notificationContainer > .details-error {
background: #fbebeb;
color: #d74345

View File

@ -152,6 +152,75 @@ loop.shared.mixins = (function() {
}
};
/**
* Media setup mixin. Provides a common location for settings for the media
* elements and handling updates of the media containers.
*/
var MediaSetupMixin = {
componentDidMount: function() {
rootObject.addEventListener('orientationchange', this.updateVideoContainer);
rootObject.addEventListener('resize', this.updateVideoContainer);
},
componentWillUnmount: function() {
rootObject.removeEventListener('orientationchange', this.updateVideoContainer);
rootObject.removeEventListener('resize', this.updateVideoContainer);
},
/**
* Used to update the video container whenever the orientation or size of the
* display area changes.
*/
updateVideoContainer: function() {
var localStreamParent = this._getElement('.local .OT_publisher');
var remoteStreamParent = this._getElement('.remote .OT_subscriber');
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
},
/**
* Returns the default configuration for publishing media on the sdk.
*
* @param {Object} options An options object containing:
* - publishVideo A boolean set to true to publish video when the stream is initiated.
*/
getDefaultPublisherConfig: function(options) {
options = options || {};
if (!"publishVideo" in options) {
throw new Error("missing option publishVideo");
}
// height set to 100%" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
return {
insertMode: "append",
width: "100%",
height: "100%",
publishVideo: options.publishVideo,
style: {
audioLevelDisplayMode: "off",
bugDisplayMode: "off",
buttonDisplayMode: "off",
nameDisplayMode: "off",
videoDisabledDisplayMode: "off"
}
};
},
/**
* Returns either the required DOMNode
*
* @param {String} className The name of the class to get the element for.
*/
_getElement: function(className) {
return this.getDOMNode().querySelector(className);
}
};
/**
* Audio mixin. Allows playing a single audio file and ensuring it
* is stopped when the component is unmounted.
@ -308,6 +377,7 @@ loop.shared.mixins = (function() {
DocumentVisibilityMixin: DocumentVisibilityMixin,
DocumentLocationMixin: DocumentLocationMixin,
DocumentTitleMixin: DocumentTitleMixin,
MediaSetupMixin: MediaSetupMixin,
UrlHashChangeMixin: UrlHashChangeMixin,
WindowCloseMixin: WindowCloseMixin
};

View File

@ -422,6 +422,27 @@ loop.shared.models = (function(l10n) {
*/
errorL10n: function(messageId, l10nProps) {
this.error(l10n.get(messageId, l10nProps));
},
/**
* Adds a success notification to the stack and renders it.
*
* @return {String} message
*/
success: function(message) {
this.add({level: "success", message: message});
},
/**
* Adds a l10n success notification to the stack and renders it.
*
* @param {String} messageId L10n message id
* @param {Object} [l10nProps] An object with variables to be interpolated
* into the translation. All members' values must be
* strings or numbers.
*/
successL10n: function(messageId, l10nProps) {
this.success(l10n.get(messageId, l10nProps));
}
});

View File

@ -141,7 +141,11 @@ loop.shared.views = (function(_, OT, l10n) {
* Conversation view.
*/
var ConversationView = React.createClass({displayName: "ConversationView",
mixins: [Backbone.Events, sharedMixins.AudioMixin],
mixins: [
Backbone.Events,
sharedMixins.AudioMixin,
sharedMixins.MediaSetupMixin
],
propTypes: {
sdk: React.PropTypes.object.isRequired,
@ -150,21 +154,6 @@ loop.shared.views = (function(_, OT, l10n) {
initiate: React.PropTypes.bool
},
// height set to 100%" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
publisherConfig: {
insertMode: "append",
width: "100%",
height: "100%",
style: {
audioLevelDisplayMode: "off",
bugDisplayMode: "off",
buttonDisplayMode: "off",
nameDisplayMode: "off",
videoDisabledDisplayMode: "off"
}
},
getDefaultProps: function() {
return {
initiate: true,
@ -180,12 +169,6 @@ loop.shared.views = (function(_, OT, l10n) {
};
},
componentWillMount: function() {
if (this.props.initiate) {
this.publisherConfig.publishVideo = this.props.video.enabled;
}
},
componentDidMount: function() {
if (this.props.initiate) {
this.listenTo(this.props.model, "session:connected",
@ -198,26 +181,6 @@ loop.shared.views = (function(_, OT, l10n) {
this.stopPublishing);
this.props.model.startSession();
}
/**
* OT inserts inline styles into the markup. Using a listener for
* resize events helps us trigger a full width/height on the element
* so that they update to the correct dimensions.
* XXX: this should be factored as a mixin.
*/
window.addEventListener('orientationchange', this.updateVideoContainer);
window.addEventListener('resize', this.updateVideoContainer);
},
updateVideoContainer: function() {
var localStreamParent = document.querySelector('.local .OT_publisher');
var remoteStreamParent = document.querySelector('.remote .OT_subscriber');
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
},
componentWillUnmount: function() {
@ -248,7 +211,10 @@ loop.shared.views = (function(_, OT, l10n) {
*/
_streamCreated: function(event) {
var incoming = this.getDOMNode().querySelector(".remote");
this.props.model.subscribe(event.stream, incoming, this.publisherConfig);
this.props.model.subscribe(event.stream, incoming,
this.getDefaultPublisherConfig({
publishVideo: this.props.video.enabled
}));
},
/**
@ -263,7 +229,7 @@ loop.shared.views = (function(_, OT, l10n) {
// XXX move this into its StreamingVideo component?
this.publisher = this.props.sdk.initPublisher(
outgoing, this.publisherConfig);
outgoing, this.getDefaultPublisherConfig({publishVideo: this.props.video.enabled}));
// Suppress OT GuM custom dialog, see bug 1018875
this.listenTo(this.publisher, "accessDialogOpened accessDenied",

View File

@ -141,7 +141,11 @@ loop.shared.views = (function(_, OT, l10n) {
* Conversation view.
*/
var ConversationView = React.createClass({
mixins: [Backbone.Events, sharedMixins.AudioMixin],
mixins: [
Backbone.Events,
sharedMixins.AudioMixin,
sharedMixins.MediaSetupMixin
],
propTypes: {
sdk: React.PropTypes.object.isRequired,
@ -150,21 +154,6 @@ loop.shared.views = (function(_, OT, l10n) {
initiate: React.PropTypes.bool
},
// height set to 100%" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
publisherConfig: {
insertMode: "append",
width: "100%",
height: "100%",
style: {
audioLevelDisplayMode: "off",
bugDisplayMode: "off",
buttonDisplayMode: "off",
nameDisplayMode: "off",
videoDisabledDisplayMode: "off"
}
},
getDefaultProps: function() {
return {
initiate: true,
@ -180,12 +169,6 @@ loop.shared.views = (function(_, OT, l10n) {
};
},
componentWillMount: function() {
if (this.props.initiate) {
this.publisherConfig.publishVideo = this.props.video.enabled;
}
},
componentDidMount: function() {
if (this.props.initiate) {
this.listenTo(this.props.model, "session:connected",
@ -198,26 +181,6 @@ loop.shared.views = (function(_, OT, l10n) {
this.stopPublishing);
this.props.model.startSession();
}
/**
* OT inserts inline styles into the markup. Using a listener for
* resize events helps us trigger a full width/height on the element
* so that they update to the correct dimensions.
* XXX: this should be factored as a mixin.
*/
window.addEventListener('orientationchange', this.updateVideoContainer);
window.addEventListener('resize', this.updateVideoContainer);
},
updateVideoContainer: function() {
var localStreamParent = document.querySelector('.local .OT_publisher');
var remoteStreamParent = document.querySelector('.remote .OT_subscriber');
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
},
componentWillUnmount: function() {
@ -248,7 +211,10 @@ loop.shared.views = (function(_, OT, l10n) {
*/
_streamCreated: function(event) {
var incoming = this.getDOMNode().querySelector(".remote");
this.props.model.subscribe(event.stream, incoming, this.publisherConfig);
this.props.model.subscribe(event.stream, incoming,
this.getDefaultPublisherConfig({
publishVideo: this.props.video.enabled
}));
},
/**
@ -263,7 +229,7 @@ loop.shared.views = (function(_, OT, l10n) {
// XXX move this into its StreamingVideo component?
this.publisher = this.props.sdk.initPublisher(
outgoing, this.publisherConfig);
outgoing, this.getDefaultPublisherConfig({publishVideo: this.props.video.enabled}));
// Suppress OT GuM custom dialog, see bug 1018875
this.listenTo(this.publisher, "accessDialogOpened accessDenied",

View File

@ -194,6 +194,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView",
mixins: [
Backbone.Events,
sharedMixins.MediaSetupMixin,
sharedMixins.RoomsAudioMixin
],
@ -231,61 +232,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
this.setState(this.props.activeRoomStore.getStoreState());
},
/**
* Returns either the required DOMNode
*
* @param {String} className The name of the class to get the element for.
*/
_getElement: function(className) {
return this.getDOMNode().querySelector(className);
},
/**
* Returns the required configuration for publishing video on the sdk.
*/
_getPublisherConfig: function() {
// height set to 100%" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
return {
insertMode: "append",
width: "100%",
height: "100%",
publishVideo: true,
style: {
audioLevelDisplayMode: "off",
bugDisplayMode: "off",
buttonDisplayMode: "off",
nameDisplayMode: "off",
videoDisabledDisplayMode: "off"
}
};
},
/**
* Used to update the video container whenever the orientation or size of the
* display area changes.
*/
updateVideoContainer: function() {
var localStreamParent = this._getElement('.local .OT_publisher');
var remoteStreamParent = this._getElement('.remote .OT_subscriber');
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
},
componentDidMount: function() {
/**
* OT inserts inline styles into the markup. Using a listener for
* resize events helps us trigger a full width/height on the element
* so that they update to the correct dimensions.
* XXX: this should be factored as a mixin, bug 1104930
*/
window.addEventListener('orientationchange', this.updateVideoContainer);
window.addEventListener('resize', this.updateVideoContainer);
// Adding a class to the document body element from here to ease styling it.
document.body.classList.add("is-standalone-room");
},
@ -305,7 +252,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this._getPublisherConfig(),
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
}));

View File

@ -194,6 +194,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
var StandaloneRoomView = React.createClass({
mixins: [
Backbone.Events,
sharedMixins.MediaSetupMixin,
sharedMixins.RoomsAudioMixin
],
@ -231,61 +232,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
this.setState(this.props.activeRoomStore.getStoreState());
},
/**
* Returns either the required DOMNode
*
* @param {String} className The name of the class to get the element for.
*/
_getElement: function(className) {
return this.getDOMNode().querySelector(className);
},
/**
* Returns the required configuration for publishing video on the sdk.
*/
_getPublisherConfig: function() {
// height set to 100%" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
return {
insertMode: "append",
width: "100%",
height: "100%",
publishVideo: true,
style: {
audioLevelDisplayMode: "off",
bugDisplayMode: "off",
buttonDisplayMode: "off",
nameDisplayMode: "off",
videoDisabledDisplayMode: "off"
}
};
},
/**
* Used to update the video container whenever the orientation or size of the
* display area changes.
*/
updateVideoContainer: function() {
var localStreamParent = this._getElement('.local .OT_publisher');
var remoteStreamParent = this._getElement('.remote .OT_subscriber');
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
},
componentDidMount: function() {
/**
* OT inserts inline styles into the markup. Using a listener for
* resize events helps us trigger a full width/height on the element
* so that they update to the correct dimensions.
* XXX: this should be factored as a mixin, bug 1104930
*/
window.addEventListener('orientationchange', this.updateVideoContainer);
window.addEventListener('resize', this.updateVideoContainer);
// Adding a class to the document body element from here to ease styling it.
document.body.classList.add("is-standalone-room");
},
@ -305,7 +252,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this._getPublisherConfig(),
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
}));

View File

@ -32,7 +32,6 @@ describe("loop.Client", function() {
.returns(null)
.withArgs("hawk-session-token")
.returns(fakeToken),
noteCallUrlExpiry: sinon.spy(),
hawkRequest: sinon.stub(),
LOOP_SESSION_TYPE: {
GUEST: 1,
@ -89,140 +88,6 @@ describe("loop.Client", function() {
});
});
describe("#requestCallUrl", function() {
it("should post to /call-url/", function() {
client.requestCallUrl("foo", callback);
sinon.assert.calledOnce(hawkRequestStub);
sinon.assert.calledWithExactly(hawkRequestStub, sinon.match.number,
"/call-url/", "POST", {callerId: "foo"}, sinon.match.func);
});
it("should send a sessionType of LOOP_SESSION_TYPE.GUEST when " +
"mozLoop.userProfile returns null", function() {
mozLoop.userProfile = null;
client.requestCallUrl("foo", callback);
sinon.assert.calledOnce(hawkRequestStub);
sinon.assert.calledWithExactly(hawkRequestStub,
mozLoop.LOOP_SESSION_TYPE.GUEST, "/call-url/", "POST",
{callerId: "foo"}, sinon.match.func);
});
it("should send a sessionType of LOOP_SESSION_TYPE.FXA when " +
"mozLoop.userProfile returns an object", function () {
mozLoop.userProfile = {};
client.requestCallUrl("foo", callback);
sinon.assert.calledOnce(hawkRequestStub);
sinon.assert.calledWithExactly(hawkRequestStub,
mozLoop.LOOP_SESSION_TYPE.FXA, "/call-url/", "POST",
{callerId: "foo"}, sinon.match.func);
});
it("should call the callback with the url when the request succeeds",
function() {
var callUrlData = {
"callUrl": "fakeCallUrl",
"expiresAt": 60
};
// Sets up the hawkRequest stub to trigger the callback with no error
// and the url.
hawkRequestStub.callsArgWith(4, null, JSON.stringify(callUrlData));
client.requestCallUrl("foo", callback);
sinon.assert.calledWithExactly(callback, null, callUrlData);
});
it("should not update call url expiry when the request succeeds",
function() {
var callUrlData = {
"callUrl": "fakeCallUrl",
"expiresAt": 6000
};
// Sets up the hawkRequest stub to trigger the callback with no error
// and the url.
hawkRequestStub.callsArgWith(4, null, JSON.stringify(callUrlData));
client.requestCallUrl("foo", callback);
sinon.assert.notCalled(mozLoop.noteCallUrlExpiry);
});
it("should call mozLoop.telemetryAdd when the request succeeds",
function(done) {
var callUrlData = {
"callUrl": "fakeCallUrl",
"expiresAt": 60
};
// Sets up the hawkRequest stub to trigger the callback with no error
// and the url.
hawkRequestStub.callsArgWith(4, null,
JSON.stringify(callUrlData));
client.requestCallUrl("foo", function(err) {
expect(err).to.be.null;
sinon.assert.calledOnce(mozLoop.telemetryAdd);
sinon.assert.calledWith(mozLoop.telemetryAdd,
"LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS",
true);
done();
});
});
it("should send an error when the request fails", function() {
// Sets up the hawkRequest stub to trigger the callback with
// an error
hawkRequestStub.callsArgWith(4, fakeErrorRes);
client.requestCallUrl("foo", callback);
sinon.assert.calledOnce(callback);
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return err.code == 400 && "invalid token" == err.message;
}));
});
it("should send an error if the data is not valid", function() {
// Sets up the hawkRequest stub to trigger the callback with
// an error
hawkRequestStub.callsArgWith(4, null, "{}");
client.requestCallUrl("foo", callback);
sinon.assert.calledOnce(callback);
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /Invalid data received/.test(err.message);
}));
});
it("should call mozLoop.telemetryAdd when the request fails",
function(done) {
// Sets up the hawkRequest stub to trigger the callback with
// an error
hawkRequestStub.callsArgWith(4, fakeErrorRes);
client.requestCallUrl("foo", function(err) {
expect(err).not.to.be.null;
sinon.assert.calledOnce(mozLoop.telemetryAdd);
sinon.assert.calledWith(mozLoop.telemetryAdd,
"LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS",
false);
done();
});
});
});
describe("#setupOutgoingCall", function() {
var calleeIds, callType;

View File

@ -16,6 +16,7 @@ describe("loop.contacts", function() {
var fakeDoneButtonText = "Fake Done";
var sandbox;
var fakeWindow;
var notifications;
beforeEach(function(done) {
sandbox = sinon.sandbox.create();
@ -59,8 +60,11 @@ describe("loop.contacts", function() {
};
navigator.mozLoop.contacts = {getAll: sandbox.stub()};
notifications = new loop.shared.models.NotificationCollection();
listView = TestUtils.renderIntoDocument(
React.createElement(loop.contacts.ContactsList));
React.createElement(loop.contacts.ContactsList, {
notifications: notifications
}));
});
afterEach(function() {
@ -84,6 +88,34 @@ describe("loop.contacts", function() {
sinon.assert.calledOnce(fakeWindow.close);
});
});
describe("#handleImportButtonClick", function() {
it("should notify the end user from a succesful import", function() {
sandbox.stub(notifications, "successL10n");
navigator.mozLoop.startImport = function(opts, cb) {
cb(null, {total: 42});
};
listView.handleImportButtonClick();
sinon.assert.calledWithExactly(
notifications.successL10n,
"import_contacts_success_message",
{total: 42});
});
it("should notify the end user from any encountered error", function() {
sandbox.stub(notifications, "errorL10n");
navigator.mozLoop.startImport = function(opts, cb) {
cb(new Error("fake error"));
};
listView.handleImportButtonClick();
sinon.assert.calledWithExactly(notifications.errorL10n,
"import_contacts_failure_message");
});
});
});
describe("ContactDetailsForm", function() {

View File

@ -92,7 +92,9 @@ describe("loop.conversationViews", function () {
fakeWindow = {
navigator: { mozLoop: fakeMozLoop },
close: sandbox.stub(),
close: sinon.stub(),
addEventListener: function() {},
removeEventListener: function() {}
};
loop.shared.mixins.setRootObject(fakeWindow);

View File

@ -53,7 +53,9 @@ describe("loop.conversation", function() {
fakeWindow = {
navigator: { mozLoop: navigator.mozLoop },
close: sandbox.stub(),
close: sinon.stub(),
addEventListener: function() {},
removeEventListener: function() {}
};
loop.shared.mixins.setRootObject(fakeWindow);

View File

@ -48,10 +48,6 @@ describe("loop.panel", function() {
getPluralForm: function() {
return "fakeText";
},
copyString: sandbox.stub(),
noteCallUrlExpiry: sinon.spy(),
composeEmail: sinon.spy(),
telemetryAdd: sinon.spy(),
contacts: {
getAll: function(callback) {
callback(null, []);
@ -186,69 +182,33 @@ describe("loop.panel", function() {
describe('TabView', function() {
var view, callTab, roomsTab, contactsTab;
describe("loop.rooms.enabled on", function() {
beforeEach(function() {
navigator.mozLoop.getLoopPref = function(pref) {
if (pref === "rooms.enabled" ||
pref === "gettingStarted.seen") {
return true;
}
};
beforeEach(function() {
navigator.mozLoop.getLoopPref = function(pref) {
if (pref === "gettingStarted.seen") {
return true;
}
};
view = createTestPanelView();
view = createTestPanelView();
[roomsTab, contactsTab] =
TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
});
it("should select contacts tab when clicking tab button", function() {
TestUtils.Simulate.click(
view.getDOMNode().querySelector("li[data-tab-name=\"contacts\"]"));
expect(contactsTab.getDOMNode().classList.contains("selected"))
.to.be.true;
});
it("should select rooms tab when clicking tab button", function() {
TestUtils.Simulate.click(
view.getDOMNode().querySelector("li[data-tab-name=\"rooms\"]"));
expect(roomsTab.getDOMNode().classList.contains("selected"))
.to.be.true;
});
[roomsTab, contactsTab] =
TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
});
describe("loop.rooms.enabled off", function() {
beforeEach(function() {
navigator.mozLoop.getLoopPref = function(pref) {
if (pref === "rooms.enabled") {
return false;
} else if (pref === "gettingStarted.seen") {
return true;
}
};
it("should select contacts tab when clicking tab button", function() {
TestUtils.Simulate.click(
view.getDOMNode().querySelector("li[data-tab-name=\"contacts\"]"));
view = createTestPanelView();
expect(contactsTab.getDOMNode().classList.contains("selected"))
.to.be.true;
});
[callTab, contactsTab] =
TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
});
it("should select rooms tab when clicking tab button", function() {
TestUtils.Simulate.click(
view.getDOMNode().querySelector("li[data-tab-name=\"rooms\"]"));
it("should select contacts tab when clicking tab button", function() {
TestUtils.Simulate.click(
view.getDOMNode().querySelector("li[data-tab-name=\"contacts\"]"));
expect(contactsTab.getDOMNode().classList.contains("selected"))
.to.be.true;
});
it("should select call tab when clicking tab button", function() {
TestUtils.Simulate.click(
view.getDOMNode().querySelector("li[data-tab-name=\"call\"]"));
expect(callTab.getDOMNode().classList.contains("selected"))
.to.be.true;
});
expect(roomsTab.getDOMNode().classList.contains("selected"))
.to.be.true;
});
});
@ -468,284 +428,6 @@ describe("loop.panel", function() {
});
});
describe("loop.panel.CallUrlResult", function() {
var fakeClient, callUrlData, view;
beforeEach(function() {
callUrlData = {
callUrl: "http://call.invalid/fakeToken",
expiresAt: 1000
};
fakeClient = {
requestCallUrl: function(_, cb) {
cb(null, callUrlData);
}
};
sandbox.stub(notifications, "reset");
view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
});
describe("Rendering the component should generate a call URL", function() {
beforeEach(function() {
document.mozL10n.initialize({
getStrings: function(key) {
var text;
if (key === "share_email_subject4")
text = "email-subject";
else if (key === "share_email_body4")
text = "{{callUrl}}";
return JSON.stringify({textContent: text});
}
});
});
it("should make a request to requestCallUrl", function() {
sandbox.stub(fakeClient, "requestCallUrl");
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
sinon.assert.calledOnce(view.props.client.requestCallUrl);
sinon.assert.calledWithExactly(view.props.client.requestCallUrl,
sinon.match.string, sinon.match.func);
});
it("should set the call url form in a pending state", function() {
// Cancel requestCallUrl effect to keep the state pending
fakeClient.requestCallUrl = sandbox.stub();
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
expect(view.state.pending).eql(true);
});
it("should update state with the call url received", function() {
expect(view.state.pending).eql(false);
expect(view.state.callUrl).eql(callUrlData.callUrl);
});
it("should clear the pending state when a response is received",
function() {
expect(view.state.pending).eql(false);
});
it("should update CallUrlResult with the call url", function() {
var urlField = view.getDOMNode().querySelector("input[type='url']");
expect(urlField.value).eql(callUrlData.callUrl);
});
it("should have 0 pending notifications", function() {
expect(view.props.notifications.length).eql(0);
});
it("should display a share button for email", function() {
fakeClient.requestCallUrl = sandbox.stub();
var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
view.setState({pending: false, callUrl: "http://example.com"});
TestUtils.findRenderedDOMComponentWithClass(view, "button-email");
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-email"));
sinon.assert.calledOnce(composeCallUrlEmail);
sinon.assert.calledWithExactly(composeCallUrlEmail, "http://example.com");
});
it("should feature a copy button capable of copying the call url when clicked", function() {
fakeClient.requestCallUrl = sandbox.stub();
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com",
callUrlExpiry: 6000
});
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-copy"));
sinon.assert.calledOnce(navigator.mozLoop.copyString);
sinon.assert.calledWithExactly(navigator.mozLoop.copyString,
view.state.callUrl);
});
it("should note the call url expiry when the url is copied via button",
function() {
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com",
callUrlExpiry: 6000
});
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-copy"));
sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
6000);
});
it("should call mozLoop.telemetryAdd when the url is copied via button",
function() {
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com",
callUrlExpiry: 6000
});
// Multiple clicks should result in the URL being counted only once.
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-copy"));
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-copy"));
sinon.assert.calledOnce(navigator.mozLoop.telemetryAdd);
sinon.assert.calledWith(navigator.mozLoop.telemetryAdd,
"LOOP_CLIENT_CALL_URL_SHARED",
true);
});
it("should note the call url expiry when the url is emailed",
function() {
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com",
callUrlExpiry: 6000
});
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-email"));
sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
6000);
});
it("should call mozLoop.telemetryAdd when the url is emailed",
function() {
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com",
callUrlExpiry: 6000
});
// Multiple clicks should result in the URL being counted only once.
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-email"));
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-email"));
sinon.assert.calledOnce(navigator.mozLoop.telemetryAdd);
sinon.assert.calledWith(navigator.mozLoop.telemetryAdd,
"LOOP_CLIENT_CALL_URL_SHARED",
true);
});
it("should note the call url expiry when the url is copied manually",
function() {
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com",
callUrlExpiry: 6000
});
var urlField = view.getDOMNode().querySelector("input[type='url']");
TestUtils.Simulate.copy(urlField);
sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
6000);
});
it("should call mozLoop.telemetryAdd when the url is copied manually",
function() {
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com",
callUrlExpiry: 6000
});
// Multiple copies should result in the URL being counted only once.
var urlField = view.getDOMNode().querySelector("input[type='url']");
TestUtils.Simulate.copy(urlField);
TestUtils.Simulate.copy(urlField);
sinon.assert.calledOnce(navigator.mozLoop.telemetryAdd);
sinon.assert.calledWith(navigator.mozLoop.telemetryAdd,
"LOOP_CLIENT_CALL_URL_SHARED",
true);
});
it("should notify the user when the operation failed", function() {
fakeClient.requestCallUrl = function(_, cb) {
cb("fake error");
};
sandbox.stub(notifications, "errorL10n");
TestUtils.renderIntoDocument(
React.createElement(loop.panel.CallUrlResult, {
notifications: notifications,
client: fakeClient
}));
sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifications.errorL10n,
"unable_retrieve_url");
});
});
});
describe("loop.panel.RoomEntry", function() {
var dispatcher, roomData;

View File

@ -20,7 +20,9 @@ describe("loop.roomViews", function () {
mozLoop: {
getAudioBlob: sinon.stub()
}
}
},
addEventListener: function() {},
removeEventListener: function() {}
};
loop.shared.mixins.setRootObject(fakeWindow);

View File

@ -20,6 +20,7 @@ describe("loop.shared.mixins", function() {
afterEach(function() {
sandbox.restore();
sharedMixins.setRootObject(window);
});
describe("loop.shared.mixins.UrlHashChangeMixin", function() {
@ -162,10 +163,6 @@ describe("loop.shared.mixins", function() {
});
});
afterEach(function() {
loop.shared.mixins.setRootObject(window);
});
function setupFakeVisibilityEventDispatcher(event) {
loop.shared.mixins.setRootObject({
document: {
@ -196,6 +193,100 @@ describe("loop.shared.mixins", function() {
});
});
describe("loop.shared.mixins.MediaSetupMixin", function() {
var view, TestComp, rootObject;
beforeEach(function() {
TestComp = React.createClass({
mixins: [loop.shared.mixins.MediaSetupMixin],
render: function() {
return React.DOM.div();
}
});
rootObject = {
events: {},
addEventListener: function(eventName, listener) {
this.events[eventName] = listener;
},
removeEventListener: function(eventName) {
delete this.events[eventName];
}
};
sharedMixins.setRootObject(rootObject);
view = TestUtils.renderIntoDocument(React.createElement(TestComp));
});
describe("#getDefaultPublisherConfig", function() {
it("should provide a default publisher configuration", function() {
var defaultConfig = view.getDefaultPublisherConfig({publishVideo: true});
expect(defaultConfig.publishVideo).eql(true);
});
});
describe("Events", function() {
var localElement, remoteElement;
beforeEach(function() {
sandbox.stub(view, "getDOMNode").returns({
querySelector: function(classSelector) {
if (classSelector.contains("local")) {
return localElement;
}
return remoteElement;
}
});
});
describe("resize", function() {
it("should update the width on the local stream element", function() {
localElement = {
style: { width: "0%" }
};
rootObject.events.resize();
expect(localElement.style.width).eql("100%");
});
it("should update the height on the remote stream element", function() {
remoteElement = {
style: { height: "0%" }
};
rootObject.events.resize();
expect(remoteElement.style.height).eql("100%");
});
});
describe("orientationchange", function() {
it("should update the width on the local stream element", function() {
localElement = {
style: { width: "0%" }
};
rootObject.events.orientationchange();
expect(localElement.style.width).eql("100%");
});
it("should update the height on the remote stream element", function() {
remoteElement = {
style: { height: "0%" }
};
rootObject.events.orientationchange();
expect(remoteElement.style.height).eql("100%");
});
});
});
});
describe("loop.shared.mixins.AudioMixin", function() {
var view, fakeAudio, TestComp;

View File

@ -265,18 +265,6 @@ describe("loop.shared.views", function() {
sinon.assert.notCalled(model.startSession);
});
it("should set the correct stream publish options", function() {
var component = mountTestComponent({
sdk: fakeSDK,
model: model,
video: {enabled: false}
});
expect(component.publisherConfig.publishVideo).to.eql(false);
});
});
describe("constructed", function() {

View File

@ -1,25 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function expiryTimePref() {
return Services.prefs.getIntPref("loop.urlsExpiryTimeSeconds");
}
function run_test()
{
setupFakeLoopServer();
Services.prefs.setIntPref("loop.urlsExpiryTimeSeconds", 0);
MozLoopService.noteCallUrlExpiry(1000);
Assert.equal(expiryTimePref(), 1000, "should be equal to set value");
MozLoopService.noteCallUrlExpiry(900);
Assert.equal(expiryTimePref(), 1000, "should remain the same value");
MozLoopService.noteCallUrlExpiry(1500);
Assert.equal(expiryTimePref(), 1500, "should be the increased value");
}

View File

@ -16,8 +16,8 @@ function test_getStrings() {
// XXX This depends on the L10n values, which I'd prefer not to do, but is the
// simplest way for now.
Assert.equal(MozLoopService.getStrings("share_link_header_text"),
'{"textContent":"Share this link to invite someone to talk:"}');
Assert.equal(MozLoopService.getStrings("display_name_guest"),
'{"textContent":"Guest"}');
}
function run_test()

View File

@ -9,7 +9,6 @@ skip-if = toolkit == 'gonk'
[test_looprooms.js]
[test_loopservice_directcall.js]
[test_loopservice_dnd.js]
[test_loopservice_expiry.js]
[test_loopservice_hawk_errors.js]
[test_loopservice_hawk_request.js]
[test_loopservice_loop_prefs.js]

View File

@ -48,14 +48,10 @@ var fakeRooms = [
* @type {Object}
*/
navigator.mozLoop = {
roomsEnabled: false,
ensureRegistered: function() {},
getAudioBlob: function(){},
getLoopPref: function(pref) {
switch(pref) {
// Ensure UI for rooms is displayed in the showcase.
case "rooms.enabled":
return this.roomsEnabled;
// Ensure we skip FTE completely.
case "gettingStarted.seen":
return true;

View File

@ -83,7 +83,6 @@
// Local mocks
var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
mockMozLoopRooms.roomsEnabled = true;
var mockContact = {
name: ["Mr Smith"],
@ -93,7 +92,6 @@
};
var mockClient = {
requestCallUrl: noop,
requestCallUrlInfo: noop
};
@ -220,33 +218,21 @@
React.createElement("p", {className: "note"},
React.createElement("strong", null, "Note:"), " 332px wide."
),
React.createElement(Example, {summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}},
React.createElement(PanelView, {client: mockClient, notifications: notifications,
callUrl: "http://invalid.example.url/",
mozLoop: navigator.mozLoop,
dispatcher: dispatcher,
roomStore: roomStore})
),
React.createElement(Example, {summary: "Call URL retrieved - authenticated", dashed: "true", style: {width: "332px"}},
React.createElement(PanelView, {client: mockClient, notifications: notifications,
callUrl: "http://invalid.example.url/",
userProfile: {email: "test@example.com"},
mozLoop: navigator.mozLoop,
dispatcher: dispatcher,
roomStore: roomStore})
),
React.createElement(Example, {summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}},
React.createElement(PanelView, {client: mockClient, notifications: notifications,
mozLoop: navigator.mozLoop,
dispatcher: dispatcher,
roomStore: roomStore})
),
React.createElement(Example, {summary: "Pending call url retrieval - authenticated", dashed: "true", style: {width: "332px"}},
React.createElement(Example, {summary: "Room list tab", dashed: "true", style: {width: "332px"}},
React.createElement(PanelView, {client: mockClient, notifications: notifications,
userProfile: {email: "test@example.com"},
mozLoop: navigator.mozLoop,
mozLoop: mockMozLoopRooms,
dispatcher: dispatcher,
roomStore: roomStore})
roomStore: roomStore,
selectedTab: "rooms"})
),
React.createElement(Example, {summary: "Contact list tab", dashed: "true", style: {width: "332px"}},
React.createElement(PanelView, {client: mockClient, notifications: notifications,
userProfile: {email: "test@example.com"},
mozLoop: mockMozLoopRooms,
dispatcher: dispatcher,
roomStore: roomStore,
selectedTab: "contacts"})
),
React.createElement(Example, {summary: "Error Notification", dashed: "true", style: {width: "332px"}},
React.createElement(PanelView, {client: mockClient, notifications: errNotifications,
@ -261,13 +247,21 @@
dispatcher: dispatcher,
roomStore: roomStore})
),
React.createElement(Example, {summary: "Room list tab", dashed: "true", style: {width: "332px"}},
React.createElement(PanelView, {client: mockClient, notifications: notifications,
React.createElement(Example, {summary: "Contact import success", dashed: "true", style: {width: "332px"}},
React.createElement(PanelView, {notifications: new loop.shared.models.NotificationCollection([{level: "success", message: "Import success"}]),
userProfile: {email: "test@example.com"},
mozLoop: mockMozLoopRooms,
dispatcher: dispatcher,
roomStore: roomStore,
selectedTab: "rooms"})
selectedTab: "contacts"})
),
React.createElement(Example, {summary: "Contact import error", dashed: "true", style: {width: "332px"}},
React.createElement(PanelView, {notifications: new loop.shared.models.NotificationCollection([{level: "error", message: "Import error"}]),
userProfile: {email: "test@example.com"},
mozLoop: mockMozLoopRooms,
dispatcher: dispatcher,
roomStore: roomStore,
selectedTab: "contacts"})
)
),

View File

@ -83,7 +83,6 @@
// Local mocks
var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
mockMozLoopRooms.roomsEnabled = true;
var mockContact = {
name: ["Mr Smith"],
@ -93,7 +92,6 @@
};
var mockClient = {
requestCallUrl: noop,
requestCallUrlInfo: noop
};
@ -220,33 +218,21 @@
<p className="note">
<strong>Note:</strong> 332px wide.
</p>
<Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications}
callUrl="http://invalid.example.url/"
mozLoop={navigator.mozLoop}
dispatcher={dispatcher}
roomStore={roomStore} />
</Example>
<Example summary="Call URL retrieved - authenticated" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications}
callUrl="http://invalid.example.url/"
userProfile={{email: "test@example.com"}}
mozLoop={navigator.mozLoop}
dispatcher={dispatcher}
roomStore={roomStore} />
</Example>
<Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications}
mozLoop={navigator.mozLoop}
dispatcher={dispatcher}
roomStore={roomStore} />
</Example>
<Example summary="Pending call url retrieval - authenticated" dashed="true" style={{width: "332px"}}>
<Example summary="Room list tab" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications}
userProfile={{email: "test@example.com"}}
mozLoop={navigator.mozLoop}
mozLoop={mockMozLoopRooms}
dispatcher={dispatcher}
roomStore={roomStore} />
roomStore={roomStore}
selectedTab="rooms" />
</Example>
<Example summary="Contact list tab" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications}
userProfile={{email: "test@example.com"}}
mozLoop={mockMozLoopRooms}
dispatcher={dispatcher}
roomStore={roomStore}
selectedTab="contacts" />
</Example>
<Example summary="Error Notification" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={errNotifications}
@ -261,13 +247,21 @@
dispatcher={dispatcher}
roomStore={roomStore} />
</Example>
<Example summary="Room list tab" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications}
<Example summary="Contact import success" dashed="true" style={{width: "332px"}}>
<PanelView notifications={new loop.shared.models.NotificationCollection([{level: "success", message: "Import success"}])}
userProfile={{email: "test@example.com"}}
mozLoop={mockMozLoopRooms}
dispatcher={dispatcher}
roomStore={roomStore}
selectedTab="rooms" />
selectedTab="contacts" />
</Example>
<Example summary="Contact import error" dashed="true" style={{width: "332px"}}>
<PanelView notifications={new loop.shared.models.NotificationCollection([{level: "error", message: "Import error"}])}
userProfile={{email: "test@example.com"}}
mozLoop={mockMozLoopRooms}
dispatcher={dispatcher}
roomStore={roomStore}
selectedTab="contacts" />
</Example>
</Section>

View File

@ -178,7 +178,10 @@ var gMainPane = {
for (let prefToChange of prefsToChange) {
prefToChange.value = e10sCheckbox.checked;
}
if (!e10sCheckbox.checked) {
let tmp = {};
Components.utils.import("resource://gre/modules/UpdateChannel.jsm", tmp);
if (!e10sCheckbox.checked && tmp.UpdateChannel.get() == "nightly") {
Services.prefs.setBoolPref("browser.requestE10sFeedback", true);
Services.prompt.alert(window, brandName, "After restart, a tab will open to input.mozilla.org where you can provide us feedback about your e10s experience.");
}

View File

@ -505,7 +505,6 @@
this.doSearch(textValue, where, aEngine);
if (!selection || (selection.index == -1)) {
let target = aEvent.originalTarget;
let source = "unknown";
let type = "unknown";
if (aEvent instanceof KeyboardEvent) {
@ -514,6 +513,7 @@
source = "oneoff";
}
} else if (aEvent instanceof MouseEvent) {
let target = aEvent.originalTarget;
type = "mouse";
if (target.classList.contains("searchbar-engine-one-off-item")) {
source = "oneoff";

View File

@ -260,6 +260,40 @@ let SessionCookiesInternal = {
}
};
/**
* Generates all possible subdomains for a given host and prepends a leading
* dot to all variants.
*
* See http://tools.ietf.org/html/rfc6265#section-5.1.3
* http://en.wikipedia.org/wiki/HTTP_cookie#Domain_and_Path
*
* All cookies belonging to a web page will be internally represented by a
* nsICookie object. nsICookie.host will be the request host if no domain
* parameter was given when setting the cookie. If a specific domain was given
* then nsICookie.host will contain that specific domain and prepend a leading
* dot to it.
*
* We thus generate all possible subdomains for a given domain and prepend a
* leading dot to them as that is the value that was used as the map key when
* the cookie was set.
*/
function* getPossibleSubdomainVariants(host) {
// Try given domain with a leading dot (.www.example.com).
yield "." + host;
// Stop if there are only two parts left (e.g. example.com was given).
let parts = host.split(".");
if (parts.length < 3) {
return;
}
// Remove the first subdomain (www.example.com -> example.com).
let rest = parts.slice(1).join(".");
// Try possible parent subdomains.
yield* getPossibleSubdomainVariants(rest);
}
/**
* The internal cookie storage that keeps track of every active session cookie.
* These are stored using maps per host, path, and cookie name.
@ -285,6 +319,11 @@ let CookieStore = {
* "/path": {
* "cookiename": {name: "cookiename", value: "value", etc...}
* }
* },
* ".example.com": {
* "/path": {
* "cookiename": {name: "cookiename", value: "value", etc...}
* }
* }
* };
*/
@ -297,14 +336,27 @@ let CookieStore = {
* A string containing the host name we want to get cookies for.
*/
getCookiesForHost: function (host) {
if (!this._hosts.has(host)) {
return [];
}
let cookies = [];
for (let pathToNamesMap of this._hosts.get(host).values()) {
cookies.push(...pathToNamesMap.values());
let appendCookiesForHost = host => {
if (!this._hosts.has(host)) {
return;
}
for (let pathToNamesMap of this._hosts.get(host).values()) {
cookies.push(...pathToNamesMap.values());
}
}
// Try to find cookies for the given host, e.g. <www.example.com>.
// The full hostname will be in the map if the Set-Cookie header did not
// have a domain= attribute, i.e. the cookie will only be stored for the
// request domain. Also, try to find cookies for subdomains, e.g.
// <.example.com>. We will find those variants with a leading dot in the
// map if the Set-Cookie header had a domain= attribute, i.e. the cookie
// will be stored for a parent domain and we send it for any subdomain.
for (let variant of [host, ...getPossibleSubdomainVariants(host)]) {
appendCookiesForHost(variant);
}
return cookies;

View File

@ -766,7 +766,7 @@ let SessionStoreInternal = {
this.saveStateDelayed(win);
break;
case "oop-browser-crashed":
this._crashedBrowsers.add(aEvent.originalTarget.permanentKey);
this.onBrowserCrashed(win, aEvent.originalTarget);
break;
}
this._clearRestoringWindows();
@ -1461,6 +1461,29 @@ let SessionStoreInternal = {
this.saveStateDelayed(aWindow);
},
/**
* Handler for the event that is fired when a <xul:browser> crashes.
*
* @param aWindow
* The window that the crashed browser belongs to.
* @param aBrowser
* The <xul:browser> that is now in the crashed state.
*/
onBrowserCrashed: function(aWindow, aBrowser) {
this._crashedBrowsers.add(aBrowser.permanentKey);
// If we never got around to restoring this tab, clear its state so
// that we don't try restoring if the user switches to it before
// reviving the crashed browser. This is throwing away the information
// that the tab was in a pending state when the browser crashed, which
// is an explicit choice. For now, when restoring all crashed tabs, based
// on a user preference we'll either restore all of them at once, or only
// restore the selected tab and lazily restore the rest. We'll make no
// efforts at this time to be smart and restore all of the tabs that had
// been in a restored state at the time of the crash.
let tab = aWindow.gBrowser.getTabForBrowser(aBrowser);
this._resetLocalTabRestoringState(tab);
},
onGatherTelemetry: function() {
// On the first gather-telemetry notification of the session,
// gather telemetry data.

View File

@ -12,6 +12,7 @@ support-files =
head.js
content.js
content-forms.js
browser_cookies.sjs
browser_formdata_sample.html
browser_formdata_xpath_sample.html
browser_frametree_sample.html
@ -65,6 +66,7 @@ support-files =
[browser_broadcast.js]
[browser_capabilities.js]
[browser_cleaner.js]
[browser_cookies.js]
[browser_crashedTabs.js]
skip-if = !e10s || os == "linux" # Waiting on OMTC enabled by default on Linux (Bug 994541)
[browser_dying_cache.js]

View File

@ -0,0 +1,175 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const PATH = "/browser/browser/components/sessionstore/test/";
/**
* Remove all cookies to start off a clean slate.
*/
add_task(function* test_setup() {
Services.cookies.removeAll();
});
/**
* Test multiple scenarios with different Set-Cookie header domain= params.
*/
add_task(function* test_run() {
// Set-Cookie: foobar=random()
// The domain of the cookie should be the request domain (www.example.com).
// We should collect data only for the request domain, no parent or subdomains.
yield testCookieCollection({
host: "http://www.example.com",
cookieHost: "www.example.com",
cookieURIs: ["http://www.example.com" + PATH],
noCookieURIs: ["http://example.com/" + PATH]
});
// Set-Cookie: foobar=random()
// The domain of the cookie should be the request domain (example.com).
// We should collect data only for the request domain, no parent or subdomains.
yield testCookieCollection({
host: "http://example.com",
cookieHost: "example.com",
cookieURIs: ["http://example.com" + PATH],
noCookieURIs: ["http://www.example.com/" + PATH]
});
// Set-Cookie: foobar=random(); Domain=example.com
// The domain of the cookie should be the given one (.example.com).
// We should collect data for the given domain and its subdomains.
yield testCookieCollection({
host: "http://example.com",
domain: "example.com",
cookieHost: ".example.com",
cookieURIs: ["http://example.com" + PATH, "http://www.example.com/" + PATH],
noCookieURIs: ["about:robots"]
});
// Set-Cookie: foobar=random(); Domain=.example.com
// The domain of the cookie should be the given one (.example.com).
// We should collect data for the given domain and its subdomains.
yield testCookieCollection({
host: "http://example.com",
domain: ".example.com",
cookieHost: ".example.com",
cookieURIs: ["http://example.com" + PATH, "http://www.example.com/" + PATH],
noCookieURIs: ["about:robots"]
});
// Set-Cookie: foobar=random(); Domain=www.example.com
// The domain of the cookie should be the given one (.www.example.com).
// We should collect data for the given domain and its subdomains.
yield testCookieCollection({
host: "http://www.example.com",
domain: "www.example.com",
cookieHost: ".www.example.com",
cookieURIs: ["http://www.example.com/" + PATH],
noCookieURIs: ["http://example.com"]
});
// Set-Cookie: foobar=random(); Domain=.www.example.com
// The domain of the cookie should be the given one (.www.example.com).
// We should collect data for the given domain and its subdomains.
yield testCookieCollection({
host: "http://www.example.com",
domain: ".www.example.com",
cookieHost: ".www.example.com",
cookieURIs: ["http://www.example.com/" + PATH],
noCookieURIs: ["http://example.com"]
});
});
/**
* Generic test function to check sessionstore's cookie collection module with
* different cookie domains given in the Set-Cookie header. See above for some
* usage examples.
*/
let testCookieCollection = Task.async(function (params) {
let tab = gBrowser.addTab("about:blank");
let browser = tab.linkedBrowser;
let urlParams = new URLSearchParams();
let value = Math.random();
urlParams.append("value", value);
if (params.domain) {
urlParams.append("domain", params.domain);
}
// Construct request URI.
let uri = `${params.host}${PATH}browser_cookies.sjs?${urlParams}`;
// Wait for the browser to load and the cookie to be set.
// These two events can probably happen in no particular order,
// so let's wait for them in parallel.
yield Promise.all([
waitForNewCookie(),
replaceCurrentURI(browser, uri)
]);
// Check all URIs for which the cookie should be collected.
for (let uri of params.cookieURIs || []) {
yield replaceCurrentURI(browser, uri);
// Check the cookie.
let cookie = getCookie();
is(cookie.host, params.cookieHost, "cookie host is correct");
is(cookie.path, PATH, "cookie path is correct");
is(cookie.name, "foobar", "cookie name is correct");
is(cookie.value, value, "cookie value is correct");
}
// Check all URIs for which the cookie should NOT be collected.
for (let uri of params.noCookieURIs || []) {
yield replaceCurrentURI(browser, uri);
// Cookie should be ignored.
ok(!getCookie(), "no cookie collected");
}
// Clean up.
gBrowser.removeTab(tab);
Services.cookies.removeAll();
});
/**
* Replace the current URI of the given browser by loading a new URI. The
* browser's session history will be completely replaced. This function ensures
* that the parent process has the lastest shistory data before resolving.
*/
let replaceCurrentURI = Task.async(function* (browser, uri) {
// Replace the tab's current URI with the parent domain.
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
browser.loadURIWithFlags(uri, flags);
yield promiseBrowserLoaded(browser);
// Ensure the tab's session history is up-to-date.
TabState.flush(browser);
});
/**
* Waits for a new "*example.com" cookie to be added.
*/
function waitForNewCookie() {
return new Promise(resolve => {
Services.obs.addObserver(function observer(subj, topic, data) {
let cookie = subj.QueryInterface(Ci.nsICookie2);
if (data == "added" && cookie.host.endsWith("example.com")) {
Services.obs.removeObserver(observer, topic);
resolve();
}
}, "cookie-changed", false);
});
}
/**
* Retrieves the first cookie in the first window from the current sessionstore
* state.
*/
function getCookie() {
let state = JSON.parse(ss.getWindowState(window));
let cookies = state.windows[0].cookies || [];
return cookies[0] || null;
}

View File

@ -0,0 +1,21 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Components.utils.importGlobalProperties(["URLSearchParams"]);
function handleRequest(req, resp) {
resp.setStatusLine(req.httpVersion, 200);
let params = new URLSearchParams(req.queryString);
let value = params.get("value");
let domain = "";
if (params.has("domain")) {
domain = `; Domain=${params.get("domain")}`;
}
resp.setHeader("Set-Cookie", `foobar=${value}${domain}`);
resp.write("<meta charset=utf-8>hi");
}

View File

@ -27,10 +27,10 @@ function test() {
outerScope.expand();
let upvarVar = outerScope.get("upvar");
ok(!upvarVar, "upvar was optimized out.");
if (upvarVar) {
ok(false, "upvar = " + upvarVar.target.querySelector(".value").getAttribute("value"));
}
ok(upvarVar, "The variable `upvar` is shown.");
is(upvarVar.target.querySelector(".value").getAttribute("value"),
gDebugger.L10N.getStr('variablesViewOptimizedOut'),
"Should show the optimized out message for upvar.");
let argVar = outerScope.get("arg");
is(argVar.target.querySelector(".name").getAttribute("value"), "arg",

View File

@ -327,17 +327,35 @@ ToolSidebar.prototype = {
}),
/**
* Show or hide a specific tab
* Show or hide a specific tab and tabpanel.
* @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it.
* @param {String} id The ID of the tab to be hidden.
* @param {String} tabPanelId Optionally pass the ID for the tabPanel if it
* can't be retrieved using the tab ID. This is useful when tabs and tabpanels
* existed before the widget was created.
*/
toggleTab: function(id, isVisible) {
toggleTab: function(isVisible, id, tabPanelId) {
// Toggle the tab.
let tab = this.getTab(id);
if (!tab) {
return;
}
tab.hidden = !isVisible;
// Toggle the item in the allTabs menu.
if (this._allTabsBtn) {
this._allTabsBtn.querySelector("#sidebar-alltabs-item-" + id).hidden = !isVisible;
}
// Toggle the corresponding tabPanel, if one can be found either with the id
// or the provided tabPanelId.
let tabPanel = this.getTabPanel(id);
if (!tabPanel && tabPanelId) {
tabPanel = this.getTabPanel(tabPanelId);
}
if (tabPanel) {
tabPanel.hidden = !isVisible;
}
},
/**

View File

@ -313,7 +313,9 @@ InspectorPanel.prototype = {
*/
setupSidebar: function InspectorPanel_setupSidebar() {
let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
this.sidebar = new ToolSidebar(tabbox, this, "inspector");
this.sidebar = new ToolSidebar(tabbox, this, "inspector", {
showAllTabsMenu: true
});
let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");

View File

@ -119,6 +119,7 @@ const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const EventEmitter = require("devtools/toolkit/event-emitter");
const Editor = require("devtools/sourceeditor/editor");
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
const {ToolSidebar} = require("devtools/framework/sidebar");
XPCOMUtils.defineLazyModuleGetter(this, "Chart",
"resource:///modules/devtools/Chart.jsm");

View File

@ -2001,6 +2001,9 @@ CustomRequestView.prototype = {
function NetworkDetailsView() {
dumpn("NetworkDetailsView was instantiated");
// The ToolSidebar requires the panel object to be able to emit events.
EventEmitter.decorate(this);
this._onTabSelect = this._onTabSelect.bind(this);
};
@ -2025,6 +2028,10 @@ NetworkDetailsView.prototype = {
dumpn("Initializing the NetworkDetailsView");
this.widget = $("#event-details-pane");
this.sidebar = new ToolSidebar(this.widget, this, "netmonitor", {
disableTelemetry: true,
showAllTabsMenu: true
});
this._headers = new VariablesView($("#all-headers"),
Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
@ -2065,7 +2072,7 @@ NetworkDetailsView.prototype = {
*/
destroy: function() {
dumpn("Destroying the NetworkDetailsView");
this.sidebar.destroy();
$("tabpanels", this.widget).removeEventListener("select", this._onTabSelect);
},
@ -2090,8 +2097,7 @@ NetworkDetailsView.prototype = {
let isHtml = RequestsMenuView.prototype.isHtml({ attachment: aData });
// Show the "Preview" tabpanel only for plain HTML responses.
$("#preview-tab").hidden = !isHtml;
$("#preview-tabpanel").hidden = !isHtml;
this.sidebar.toggleTab(isHtml, "preview-tab", "preview-tabpanel");
// Show the "Security" tab only for requests that
// 1) are https (state != insecure)

View File

@ -119,7 +119,6 @@ function isValidSerializerVersion (version) {
].indexOf(version);
}
/**
* Takes recording data (with version `1`, from the original profiler tool), and
* massages the data to be line with the current performance tool's property names

View File

@ -23,6 +23,13 @@ devtools.lazyRequireGetter(this, "L10N",
"devtools/profiler/global", true);
devtools.lazyRequireGetter(this, "PerformanceIO",
"devtools/performance/io", true);
devtools.lazyRequireGetter(this, "RecordingModel",
"devtools/performance/recording-model", true);
devtools.lazyRequireGetter(this, "RECORDING_IN_PROGRESS",
"devtools/performance/recording-model", true);
devtools.lazyRequireGetter(this, "RECORDING_UNAVAILABLE",
"devtools/performance/recording-model", true);
devtools.lazyRequireGetter(this, "MarkersOverview",
"devtools/timeline/markers-overview", true);
devtools.lazyRequireGetter(this, "MemoryOverview",
@ -35,22 +42,19 @@ devtools.lazyRequireGetter(this, "CallView",
"devtools/profiler/tree-view", true);
devtools.lazyRequireGetter(this, "ThreadNode",
"devtools/profiler/tree-model", true);
devtools.lazyRequireGetter(this, "FrameNode",
"devtools/profiler/tree-model", true);
devtools.lazyImporter(this, "CanvasGraphUtils",
"resource:///modules/devtools/Graphs.jsm");
devtools.lazyImporter(this, "LineGraphWidget",
"resource:///modules/devtools/Graphs.jsm");
devtools.lazyImporter(this, "SideMenuWidget",
"resource:///modules/devtools/SideMenuWidget.jsm");
const { RecordingModel, RECORDING_IN_PROGRESS, RECORDING_UNAVAILABLE } =
devtools.require("devtools/performance/recording-model");
devtools.lazyImporter(this, "FlameGraphUtils",
"resource:///modules/devtools/FlameGraph.jsm");
devtools.lazyImporter(this, "FlameGraph",
"resource:///modules/devtools/FlameGraph.jsm");
devtools.lazyImporter(this, "SideMenuWidget",
"resource:///modules/devtools/SideMenuWidget.jsm");
// Events emitted by various objects in the panel.
const EVENTS = {
@ -203,11 +207,11 @@ let PerformanceController = {
* when the front has started to record.
*/
startRecording: Task.async(function *() {
let model = this.createNewRecording();
this.setCurrentRecording(model);
yield model.startRecording();
let recording = this.createNewRecording();
this.setCurrentRecording(recording);
yield recording.startRecording();
this.emit(EVENTS.RECORDING_STARTED, model);
this.emit(EVENTS.RECORDING_STARTED, recording);
}),
/**
@ -215,7 +219,7 @@ let PerformanceController = {
* when the front has stopped recording.
*/
stopRecording: Task.async(function *() {
let recording = this._getLatest();
let recording = this._getLatestRecording();
yield recording.stopRecording();
this.emit(EVENTS.RECORDING_STOPPED, recording);
@ -243,28 +247,33 @@ let PerformanceController = {
* The file to import the data from.
*/
importRecording: Task.async(function*(_, file) {
let model = this.createNewRecording();
yield model.importRecording(file);
let recording = this.createNewRecording();
yield recording.importRecording(file);
this.emit(EVENTS.RECORDING_IMPORTED, model.getAllData(), model);
this.emit(EVENTS.RECORDING_IMPORTED, recording);
}),
/**
* Creates a new RecordingModel, fires events and stores it
* internally in the controller.
*
* @return RecordingModel
* The newly created recording model.
*/
createNewRecording: function () {
let model = new RecordingModel({
let recording = new RecordingModel({
front: gFront,
performance: performance
});
this._recordings.push(model);
this.emit(EVENTS.RECORDING_CREATED, model);
return model;
this._recordings.push(recording);
this.emit(EVENTS.RECORDING_CREATED, recording);
return recording;
},
/**
* Sets the active RecordingModel to `recording`.
* Sets the currently active RecordingModel.
* @param RecordingModel recording
*/
setCurrentRecording: function (recording) {
if (this._currentRecording !== recording) {
@ -274,79 +283,18 @@ let PerformanceController = {
},
/**
* Return the current active RecordingModel.
* Gets the currently active RecordingModel.
* @return RecordingModel
*/
getCurrentRecording: function () {
return this._currentRecording;
},
/**
* Gets the amount of time elapsed locally after starting a recording.
* Get most recently added recording that was triggered manually (via UI).
* @return RecordingModel
*/
getLocalElapsedTime: function () {
return this.getCurrentRecording().getLocalElapsedTime;
},
/**
* Gets the time interval for the current recording.
* @return object
*/
getInterval: function() {
return this.getCurrentRecording().getInterval();
},
/**
* Gets the accumulated markers in the current recording.
* @return array
*/
getMarkers: function() {
return this.getCurrentRecording().getMarkers();
},
/**
* Gets the accumulated stack frames in the current recording.
* @return array
*/
getFrames: function() {
return this.getCurrentRecording().getFrames();
},
/**
* Gets the accumulated memory measurements in this recording.
* @return array
*/
getMemory: function() {
return this.getCurrentRecording().getMemory();
},
/**
* Gets the accumulated refresh driver ticks in this recording.
* @return array
*/
getTicks: function() {
return this.getCurrentRecording().getTicks();
},
/**
* Gets the profiler data in this recording.
* @return array
*/
getProfilerData: function() {
return this.getCurrentRecording().getProfilerData();
},
/**
* Gets all the data in this recording.
*/
getAllData: function() {
return this.getCurrentRecording().getAllData();
},
/**
/**
* Get most recently added profile that was triggered manually (via UI)
*/
_getLatest: function () {
_getLatestRecording: function () {
for (let i = this._recordings.length - 1; i >= 0; i--) {
return this._recordings[i];
}
@ -362,8 +310,8 @@ let PerformanceController = {
},
/**
* Fired from RecordingsView, we listen on the PerformanceController
* so we can set it here and re-emit on the controller, where all views can listen.
* Fired from RecordingsView, we listen on the PerformanceController so we can
* set it here and re-emit on the controller, where all views can listen.
*/
_onRecordingSelectFromView: function (_, recording) {
this.setCurrentRecording(recording);
@ -379,7 +327,9 @@ EventEmitter.decorate(PerformanceController);
* Shortcuts for accessing various profiler preferences.
*/
const Prefs = new ViewHelpers.Prefs("devtools.profiler", {
showPlatformData: ["Bool", "ui.show-platform-data"]
flattenTreeRecursion: ["Bool", "ui.flatten-tree-recursion"],
showPlatformData: ["Bool", "ui.show-platform-data"],
showIdleBlocks: ["Bool", "ui.show-idle-blocks"],
});
/**

View File

@ -9,7 +9,7 @@ function spawnTest () {
let { EVENTS, PerformanceController, FlameGraphView } = panel.panelWin;
yield startRecording(panel);
yield waitUntil(() => PerformanceController.getMarkers().length);
yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length);
let rendered = once(FlameGraphView, EVENTS.FLAMEGRAPH_RENDERED);
yield stopRecording(panel);

View File

@ -9,7 +9,7 @@ function spawnTest () {
let { EVENTS, PerformanceController, WaterfallView } = panel.panelWin;
yield startRecording(panel);
yield waitUntil(() => PerformanceController.getMarkers().length);
yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length);
let rendered = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
yield stopRecording(panel);

View File

@ -14,7 +14,7 @@ let test = Task.async(function*() {
// Verify original recording.
let originalData = PerformanceController.getAllData();
let originalData = PerformanceController.getCurrentRecording().getAllData();
ok(originalData, "The original recording is not empty.");
// Save recording.
@ -42,7 +42,7 @@ let test = Task.async(function*() {
// Verify imported recording.
let importedData = PerformanceController.getAllData();
let importedData = PerformanceController.getCurrentRecording().getAllData();
is(importedData.startTime, originalData.startTime,
"The impored data is identical to the original data (1).");

View File

@ -14,7 +14,7 @@ let test = Task.async(function*() {
yield stopRecording(panel);
// Get data from the current profiler
let data = PerformanceController.getAllData();
let data = PerformanceController.getCurrentRecording().getAllData();
// Create a structure from the data that mimics the old profiler's data.
// Different name for `ticks`, different way of storing time,
@ -46,7 +46,7 @@ let test = Task.async(function*() {
// Verify imported recording.
let importedData = PerformanceController.getAllData();
let importedData = PerformanceController.getCurrentRecording().getAllData();
is(importedData.startTime, data.startTime,
"The imported legacy data was successfully converted for the current tool (1).");

View File

@ -73,7 +73,8 @@ let CallTreeView = {
_onRangeChange: function (_, params) {
// When a range is cleared, we'll have no beginAt/endAt data,
// so the rebuild will just render all the data again.
let profilerData = PerformanceController.getProfilerData();
let recording = PerformanceController.getCurrentRecording();
let profilerData = recording.getProfilerData();
let { beginAt, endAt } = params || {};
this.render(profilerData, beginAt, endAt);
},

View File

@ -42,7 +42,11 @@ let FlameGraphView = {
return;
}
let samples = profilerData.profile.threads[0].samples;
let dataSrc = FlameGraphUtils.createFlameGraphDataFromSamples(samples);
let dataSrc = FlameGraphUtils.createFlameGraphDataFromSamples(samples, {
flattenRecursion: Prefs.flattenTreeRecursion,
filterFrames: !Prefs.showPlatformData && FrameNode.isContent,
showIdleBlocks: Prefs.showIdleBlocks && L10N.getStr("table.idle")
});
this.graph.setData(dataSrc);
this.emit(EVENTS.FLAMEGRAPH_RENDERED);
},

View File

@ -48,8 +48,9 @@ let WaterfallView = {
* Method for handling all the set up for rendering a new waterfall.
*/
render: function() {
let { startTime, endTime } = PerformanceController.getInterval();
let markers = PerformanceController.getMarkers();
let recording = PerformanceController.getCurrentRecording();
let { startTime, endTime } = recording.getInterval();
let markers = recording.getMarkers();
this.waterfall.setData(markers, startTime, startTime, endTime);
@ -84,12 +85,11 @@ let WaterfallView = {
* updating the markers detail view.
*/
_onMarkerSelected: function (event, marker) {
let recording = PerformanceController.getCurrentRecording();
let frames = recording.getFrames();
if (event === "selected") {
this.details.render({
toolbox: gToolbox,
marker: marker,
frames: PerformanceController.getFrames()
});
this.details.render({ toolbox: gToolbox, marker, frames });
}
if (event === "unselected") {
this.details.empty();

View File

@ -112,10 +112,11 @@ let OverviewView = {
* The fps graph resolution. @see Graphs.jsm
*/
render: Task.async(function *(resolution) {
let interval = PerformanceController.getInterval();
let markers = PerformanceController.getMarkers();
let memory = PerformanceController.getMemory();
let timestamps = PerformanceController.getTicks();
let recording = PerformanceController.getCurrentRecording();
let interval = recording.getInterval();
let markers = recording.getMarkers();
let memory = recording.getMemory();
let timestamps = recording.getTicks();
this.markersOverview.setData({ interval, markers });
this.emit(EVENTS.MARKERS_GRAPH_RENDERED);

View File

@ -151,12 +151,10 @@ let RecordingsView = Heritage.extend(WidgetMethods, {
/**
* Signals that a recording has been imported.
*
* @param object recordingData
* The profiler and refresh driver ticks data received from the front.
* @param RecordingModel model
* The recording model containing data on the recording session.
*/
_onRecordingImported: function (_, recordingData, model) {
_onRecordingImported: function (_, model) {
let recordingItem = this.addEmptyRecording(model);
recordingItem.isRecording = false;

View File

@ -7,44 +7,44 @@
*/
function test() {
let { _isContent } = devtools.require("devtools/profiler/tree-model");
let { FrameNode } = devtools.require("devtools/profiler/tree-model");
ok(_isContent({ location: "http://foo" }),
ok(FrameNode.isContent({ location: "http://foo" }),
"Verifying content/chrome frames is working properly.");
ok(_isContent({ location: "https://foo" }),
ok(FrameNode.isContent({ location: "https://foo" }),
"Verifying content/chrome frames is working properly.");
ok(_isContent({ location: "file://foo" }),
ok(FrameNode.isContent({ location: "file://foo" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ location: "chrome://foo" }),
ok(!FrameNode.isContent({ location: "chrome://foo" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ location: "resource://foo" }),
ok(!FrameNode.isContent({ location: "resource://foo" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ location: "chrome://foo -> http://bar" }),
ok(!FrameNode.isContent({ location: "chrome://foo -> http://bar" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ location: "chrome://foo -> https://bar" }),
ok(!FrameNode.isContent({ location: "chrome://foo -> https://bar" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ location: "chrome://foo -> file://bar" }),
ok(!FrameNode.isContent({ location: "chrome://foo -> file://bar" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ location: "resource://foo -> http://bar" }),
ok(!FrameNode.isContent({ location: "resource://foo -> http://bar" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ location: "resource://foo -> https://bar" }),
ok(!FrameNode.isContent({ location: "resource://foo -> https://bar" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ location: "resource://foo -> file://bar" }),
ok(!FrameNode.isContent({ location: "resource://foo -> file://bar" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ category: 1, location: "chrome://foo" }),
ok(!FrameNode.isContent({ category: 1, location: "chrome://foo" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ category: 1, location: "resource://foo" }),
ok(!FrameNode.isContent({ category: 1, location: "resource://foo" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ category: 1, location: "file://foo -> http://bar" }),
ok(!FrameNode.isContent({ category: 1, location: "file://foo -> http://bar" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ category: 1, location: "file://foo -> https://bar" }),
ok(!FrameNode.isContent({ category: 1, location: "file://foo -> https://bar" }),
"Verifying content/chrome frames is working properly.");
ok(!_isContent({ category: 1, location: "file://foo -> file://bar" }),
ok(!FrameNode.isContent({ category: 1, location: "file://foo -> file://bar" }),
"Verifying content/chrome frames is working properly.");
finish();

View File

@ -18,7 +18,7 @@ const CONTENT_SCHEMES = ["http://", "https://", "file://"];
exports.ThreadNode = ThreadNode;
exports.FrameNode = FrameNode;
exports._isContent = isContent; // used in tests
exports.FrameNode.isContent = isContent;
/**
* A call tree for a thread. This is essentially a linkage between all frames

View File

@ -39,6 +39,7 @@ EXTRA_JS_MODULES.devtools.shared += [
'frame-script-utils.js',
'inplace-editor.js',
'observable-object.js',
'options-view.js',
'telemetry.js',
'theme-switching.js',
'theme.js',

View File

@ -0,0 +1,165 @@
const EventEmitter = require("devtools/toolkit/event-emitter");
const { Services } = require("resource://gre/modules/Services.jsm");
const OPTIONS_SHOWN_EVENT = "options-shown";
const OPTIONS_HIDDEN_EVENT = "options-hidden";
const PREF_CHANGE_EVENT = "pref-changed";
/**
* OptionsView constructor. Takes several options, all required:
* - branchName: The name of the prefs branch, like "devtools.debugger."
* - window: The window the XUL elements live in.
* - menupopup: The XUL `menupopup` item that contains the pref buttons.
*
* Fires an event, PREF_CHANGE_EVENT, with the preference name that changed as the second
* argument. Fires events on opening/closing the XUL panel (OPTIONS_SHOW_EVENT, OPTIONS_HIDDEN_EVENT)
* as the second argument in the listener, used for tests mostly.
*/
const OptionsView = function (options={}) {
this.branchName = options.branchName;
this.window = options.window;
this.menupopup = options.menupopup;
let { document } = this.window;
this.$ = document.querySelector.bind(document);
this.$$ = document.querySelectorAll.bind(document);
this.prefObserver = new PrefObserver(this.branchName);
EventEmitter.decorate(this);
};
exports.OptionsView = OptionsView;
OptionsView.prototype = {
/**
* Binds the events and observers for the OptionsView.
*/
initialize: function () {
let { MutationObserver } = this.window;
this._onPrefChange = this._onPrefChange.bind(this);
this._onOptionChange = this._onOptionChange.bind(this);
this._onPopupShown = this._onPopupShown.bind(this);
this._onPopupHidden = this._onPopupHidden.bind(this);
// We use a mutation observer instead of a click handler
// because the click handler is fired before the XUL menuitem updates
// it's checked status, which cascades incorrectly with the Preference observer.
this.mutationObserver = new MutationObserver(this._onOptionChange);
let observerConfig = { attributes: true, attributeFilter: ["checked"]};
// Sets observers and default options for all options
for (let $el of this.$$("menuitem", this.menupopup)) {
let prefName = $el.getAttribute("data-pref");
if (this.prefObserver.get(prefName)) {
$el.setAttribute("checked", "true");
} else {
$el.removeAttribute("checked");
}
this.mutationObserver.observe($el, observerConfig);
}
// Listen to any preference change in the specified branch
this.prefObserver.register();
this.prefObserver.on(PREF_CHANGE_EVENT, this._onPrefChange);
// Bind to menupopup's open and close event
this.menupopup.addEventListener("popupshown", this._onPopupShown);
this.menupopup.addEventListener("popuphidden", this._onPopupHidden);
},
/**
* Removes event handlers for all of the option buttons and
* preference observer.
*/
destroy: function () {
this.mutationObserver.disconnect();
this.prefObserver.off(PREF_CHANGE_EVENT, this._onPrefChange);
this.menupopup.removeEventListener("popupshown", this._onPopupShown);
this.menupopup.removeEventListener("popuphidden", this._onPopupHidden);
},
/**
* Called when a preference is changed (either via clicking an option
* button or by changing it in about:config). Updates the checked status
* of the corresponding button.
*/
_onPrefChange: function (_, prefName) {
let $el = this.$(`menuitem[data-pref="${prefName}"]`, this.menupopup);
let value = this.prefObserver.get(prefName);
if (value) {
$el.setAttribute("checked", value);
} else {
$el.removeAttribute("checked");
}
this.emit(PREF_CHANGE_EVENT, prefName);
},
/**
* Mutation handler for handling a change on an options button.
* Sets the preference accordingly.
*/
_onOptionChange: function (mutations) {
let { target } = mutations[0];
let prefName = target.getAttribute("data-pref");
let value = target.getAttribute("checked") === "true";
this.prefObserver.set(prefName, value);
},
/**
* Fired when the `menupopup` is opened, bound via XUL.
* Fires an event used in tests.
*/
_onPopupShown: function () {
this.emit(OPTIONS_SHOWN_EVENT);
},
/**
* Fired when the `menupopup` is closed, bound via XUL.
* Fires an event used in tests.
*/
_onPopupHidden: function () {
this.emit(OPTIONS_HIDDEN_EVENT);
}
};
/**
* Constructor for PrefObserver. Small helper for observing changes
* on a preference branch. Takes a `branchName`, like "devtools.debugger."
*
* Fires an event of PREF_CHANGE_EVENT with the preference name that changed
* as the second argument in the listener.
*/
const PrefObserver = function (branchName) {
this.branchName = branchName;
this.branch = Services.prefs.getBranch(branchName);
EventEmitter.decorate(this);
};
PrefObserver.prototype = {
/**
* Returns `prefName`'s value. Does not require the branch name.
*/
get: function (prefName) {
let fullName = this.branchName + prefName;
return Services.prefs.getBoolPref(fullName);
},
/**
* Sets `prefName`'s `value`. Does not require the branch name.
*/
set: function (prefName, value) {
let fullName = this.branchName + prefName;
Services.prefs.setBoolPref(fullName, value);
},
register: function () {
this.branch.addObserver("", this, false);
},
unregister: function () {
this.branch.removeObserver("", this);
},
observe: function (subject, topic, prefName) {
this.emit(PREF_CHANGE_EVENT, prefName);
}
};

View File

@ -8,6 +8,7 @@ support-files =
browser_templater_basic.html
browser_toolbar_basic.html
browser_toolbar_webconsole_errors_count.html
doc_options-view.xul
head.js
leakhunt.js
@ -20,7 +21,10 @@ support-files =
[browser_flame-graph-03a.js]
[browser_flame-graph-03b.js]
[browser_flame-graph-04.js]
[browser_flame-graph-utils.js]
[browser_flame-graph-utils-01.js]
[browser_flame-graph-utils-02.js]
[browser_flame-graph-utils-03.js]
[browser_flame-graph-utils-04.js]
[browser_graphs-01.js]
[browser_graphs-02.js]
[browser_graphs-03.js]
@ -85,3 +89,4 @@ skip-if = buildapp == 'mulet'
[browser_treeWidget_basic.js]
[browser_treeWidget_keyboard_interaction.js]
[browser_treeWidget_mouse_interaction.js]
[browser_options-view-01.js]

View File

@ -1,7 +1,8 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that text metrics in the flame graph widget work properly.
// Tests that text metrics and data conversion from profiler samples
// widget work properly in the flame graph.
let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});

View File

@ -0,0 +1,104 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests consecutive duplicate frames are removed from the flame graph data.
let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
let test = Task.async(function*() {
yield promiseTab("about:blank");
yield performTest();
gBrowser.removeCurrentTab();
finish();
});
function* performTest() {
let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, {
flattenRecursion: true
});
ok(out, "Some data was outputted properly");
is(out.length, 10, "The outputted length is correct.");
info("Got flame graph data:\n" + out.toSource() + "\n");
for (let i = 0; i < out.length; i++) {
let found = out[i];
let expected = EXPECTED_OUTPUT[i];
is(found.blocks.length, expected.blocks.length,
"The correct number of blocks were found in this bucket.");
for (let j = 0; j < found.blocks.length; j++) {
is(found.blocks[j].x, expected.blocks[j].x,
"The expected block X position is correct for this frame.");
is(found.blocks[j].y, expected.blocks[j].y,
"The expected block Y position is correct for this frame.");
is(found.blocks[j].width, expected.blocks[j].width,
"The expected block width is correct for this frame.");
is(found.blocks[j].height, expected.blocks[j].height,
"The expected block height is correct for this frame.");
is(found.blocks[j].text, expected.blocks[j].text,
"The expected block text is correct for this frame.");
}
}
}
let TEST_DATA = [{
frames: [{
location: "A"
}, {
location: "A"
}, {
location: "A"
}, {
location: "B",
}, {
location: "B",
}, {
location: "C"
}],
time: 50,
}];
let EXPECTED_OUTPUT = [{
blocks: []
}, {
blocks: []
}, {
blocks: [{
srcData: {
startTime: 0,
rawLocation: "A"
},
x: 0,
y: 0,
width: 50,
height: 11,
text: "A"
}]
}, {
blocks: [{
srcData: {
startTime: 0,
rawLocation: "B"
},
x: 0,
y: 11,
width: 50,
height: 11,
text: "B"
}]
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}];

View File

@ -0,0 +1,113 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests if platform frames are removed from the flame graph data.
let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
let {FrameNode} = devtools.require("devtools/profiler/tree-model");
let test = Task.async(function*() {
yield promiseTab("about:blank");
yield performTest();
gBrowser.removeCurrentTab();
finish();
});
function* performTest() {
let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, {
filterFrames: FrameNode.isContent
});
ok(out, "Some data was outputted properly");
is(out.length, 10, "The outputted length is correct.");
info("Got flame graph data:\n" + out.toSource() + "\n");
for (let i = 0; i < out.length; i++) {
let found = out[i];
let expected = EXPECTED_OUTPUT[i];
is(found.blocks.length, expected.blocks.length,
"The correct number of blocks were found in this bucket.");
for (let j = 0; j < found.blocks.length; j++) {
is(found.blocks[j].x, expected.blocks[j].x,
"The expected block X position is correct for this frame.");
is(found.blocks[j].y, expected.blocks[j].y,
"The expected block Y position is correct for this frame.");
is(found.blocks[j].width, expected.blocks[j].width,
"The expected block width is correct for this frame.");
is(found.blocks[j].height, expected.blocks[j].height,
"The expected block height is correct for this frame.");
is(found.blocks[j].text, expected.blocks[j].text,
"The expected block text is correct for this frame.");
}
}
}
let TEST_DATA = [{
frames: [{
location: "http://A"
}, {
location: "https://B"
}, {
location: "file://C",
}, {
location: "chrome://D"
}, {
location: "resource://E"
}],
time: 50,
}];
let EXPECTED_OUTPUT = [{
blocks: []
}, {
blocks: []
}, {
blocks: [{
srcData: {
startTime: 0,
rawLocation: "http://A"
},
x: 0,
y: 0,
width: 50,
height: 11,
text: "http://A"
}, {
srcData: {
startTime: 0,
rawLocation: "file://C"
},
x: 0,
y: 22,
width: 50,
height: 11,
text: "file://C"
}]
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: [{
srcData: {
startTime: 0,
rawLocation: "https://B"
},
x: 0,
y: 11,
width: 50,
height: 11,
text: "https://B"
}]
}, {
blocks: []
}];

View File

@ -0,0 +1,167 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests if (idle) nodes are added when necessary in the flame graph data.
let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
let {FrameNode} = devtools.require("devtools/profiler/tree-model");
let test = Task.async(function*() {
yield promiseTab("about:blank");
yield performTest();
gBrowser.removeCurrentTab();
finish();
});
function* performTest() {
let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, {
flattenRecursion: true,
filterFrames: FrameNode.isContent,
showIdleBlocks: "\m/"
});
ok(out, "Some data was outputted properly");
is(out.length, 10, "The outputted length is correct.");
info("Got flame graph data:\n" + out.toSource() + "\n");
for (let i = 0; i < out.length; i++) {
let found = out[i];
let expected = EXPECTED_OUTPUT[i];
is(found.blocks.length, expected.blocks.length,
"The correct number of blocks were found in this bucket.");
for (let j = 0; j < found.blocks.length; j++) {
is(found.blocks[j].x, expected.blocks[j].x,
"The expected block X position is correct for this frame.");
is(found.blocks[j].y, expected.blocks[j].y,
"The expected block Y position is correct for this frame.");
is(found.blocks[j].width, expected.blocks[j].width,
"The expected block width is correct for this frame.");
is(found.blocks[j].height, expected.blocks[j].height,
"The expected block height is correct for this frame.");
is(found.blocks[j].text, expected.blocks[j].text,
"The expected block text is correct for this frame.");
}
}
}
let TEST_DATA = [{
frames: [{
location: "http://A"
}, {
location: "http://A"
}, {
location: "http://A"
}, {
location: "https://B"
}, {
location: "https://B"
}, {
location: "file://C",
}, {
location: "chrome://D"
}, {
location: "resource://E"
}],
time: 50
}, {
frames: [{
location: "chrome://D"
}, {
location: "resource://E"
}],
time: 100
}, {
frames: [{
location: "http://A"
}, {
location: "https://B"
}, {
location: "file://C",
}],
time: 150
}];
let EXPECTED_OUTPUT = [{
blocks: []
}, {
blocks: []
}, {
blocks: [{
srcData: {
startTime: 0,
rawLocation: "http://A"
},
x: 0,
y: 0,
width: 50,
height: 11,
text: "http://A"
}, {
srcData: {
startTime: 0,
rawLocation: "file://C"
},
x: 0,
y: 22,
width: 50,
height: 11,
text: "file://C"
}, {
srcData: {
startTime: 100,
rawLocation: "http://A"
},
x: 100,
y: 0,
width: 50,
height: 11,
text: "http://A"
}]
}, {
blocks: [{
srcData: {
startTime: 50,
rawLocation: "\m/"
},
x: 50,
y: 0,
width: 50,
height: 11,
text: "\m/"
}]
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: [{
srcData: {
startTime: 0,
rawLocation: "https://B"
},
x: 0,
y: 11,
width: 50,
height: 11,
text: "https://B"
}, {
srcData: {
startTime: 100,
rawLocation: "https://B"
},
x: 100,
y: 11,
width: 50,
height: 11,
text: "https://B"
}]
}, {
blocks: []
}];

View File

@ -0,0 +1,101 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that options-view OptionsView responds to events correctly.
let { OptionsView } = devtools.require("devtools/shared/options-view");
let { Services } = devtools.require("resource://gre/modules/Services.jsm");
const BRANCH = "devtools.debugger.";
const BLACK_BOX_PREF = "auto-black-box";
const PRETTY_PRINT_PREF = "auto-pretty-print";
let originalBlackBox = Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF);
let originalPrettyPrint = Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF);
let test = Task.async(function*() {
Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, false);
Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, true);
let tab = yield promiseTab(OPTIONS_VIEW_URL);
yield testOptionsView(tab);
gBrowser.removeCurrentTab();
cleanup();
finish();
});
function* testOptionsView(tab) {
let events = [];
let options = createOptionsView(tab);
yield options.initialize();
let window = tab._contentWindow;
let $ = window.document.querySelector.bind(window.document);
options.on("pref-changed", (_, pref) => events.push(pref));
let ppEl = $("menuitem[data-pref='auto-pretty-print']");
let bbEl = $("menuitem[data-pref='auto-black-box']");
// Test default config
is(ppEl.getAttribute("checked"), "true", "`true` prefs are checked on start");
is(bbEl.getAttribute("checked"), "", "`false` prefs are unchecked on start");
// Test buttons update when preferences update outside of the menu
Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, false);
Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, true);
is(ppEl.getAttribute("checked"), "", "menuitems update when preferences change");
is(bbEl.getAttribute("checked"), "true", "menuitems update when preferences change");
// Tests events are fired when preferences update outside of the menu
is(events.length, 2, "two 'pref-changed' events fired");
is(events[0], "auto-pretty-print", "correct pref passed in 'pref-changed' event (auto-pretty-print)");
is(events[1], "auto-black-box", "correct pref passed in 'pref-changed' event (auto-black-box)");
// Test buttons update when clicked and preferences are updated
yield click(options, window, ppEl);
is(ppEl.getAttribute("checked"), "true", "menuitems update when clicked");
is(Services.prefs.getBoolPref(BRANCH + PRETTY_PRINT_PREF), true, "preference updated via click");
yield click(options, window, bbEl);
is(bbEl.getAttribute("checked"), "", "menuitems update when clicked");
is(Services.prefs.getBoolPref(BRANCH + BLACK_BOX_PREF), false, "preference updated via click");
// Tests events are fired when preferences updated via click
is(events.length, 4, "two 'pref-changed' events fired");
is(events[2], "auto-pretty-print", "correct pref passed in 'pref-changed' event (auto-pretty-print)");
is(events[3], "auto-black-box", "correct pref passed in 'pref-changed' event (auto-black-box)");
}
function wait(window) {
return new Promise(function (resolve, reject) {
window.setTimeout(() => resolve, 60000);
});
}
function createOptionsView (tab) {
return new OptionsView({
branchName: BRANCH,
window: tab._contentWindow,
menupopup: tab._contentWindow.document.querySelector("#options-menupopup")
});
}
function cleanup () {
Services.prefs.setBoolPref(BRANCH + BLACK_BOX_PREF, originalBlackBox);
Services.prefs.setBoolPref(BRANCH + PRETTY_PRINT_PREF, originalPrettyPrint);
}
function* click (view, win, menuitem) {
let opened = view.once("options-shown");
let closed = view.once("options-hidden");
let button = win.document.querySelector("#options-button");
EventUtils.synthesizeMouseAtCenter(button, {}, win);
yield opened;
EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
yield closed;
}
function* openMenu (view, win) {
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0"?>
<!-- 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/. -->
<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
<!DOCTYPE window []>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<popupset id="options-popupset">
<menupopup id="options-menupopup" position="before_end">
<menuitem id="option-autoprettyprint"
type="checkbox"
data-pref="auto-pretty-print"
label="pretty print"/>
<menuitem id="option-autoblackbox"
type="checkbox"
data-pref="auto-black-box"
label="black box"/>
</menupopup>
</popupset>
<button id="options-button"
popup="options-menupopup"/>
</window>

View File

@ -12,6 +12,7 @@ SimpleTest.registerCleanupFunction(() => {
});
const TEST_URI_ROOT = "http://example.com/browser/browser/devtools/shared/test/";
const OPTIONS_VIEW_URL = TEST_URI_ROOT + "doc_options-view.xul";
/**
* Open a new tab at a URL and call a callback on load

View File

@ -827,12 +827,20 @@ let FlameGraphUtils = {
*
* @param array samples
* A list of { time, frames: [{ location }] } objects.
* @param object options [optional]
* Additional options supported by this operation:
* - flattenRecursion: specifies if identical consecutive frames
* should be omitted from the output
* - filterFrames: predicate used for filtering all frames, passing
* in each frame, its index and the sample array
* - showIdleBlocks: adds "idle" blocks when no frames are available
* using the provided localized text
* @param array out [optional]
* An output storage to reuse for storing the flame graph data.
* @return array
* The flame graph data.
*/
createFlameGraphDataFromSamples: function(samples, out = []) {
createFlameGraphDataFromSamples: function(samples, options = {}, out = []) {
// 1. Create a map of colors to arrays, representing buckets of
// blocks inside the flame graph pyramid sharing the same style.
@ -850,6 +858,24 @@ let FlameGraphUtils = {
for (let { frames, time } of samples) {
let frameIndex = 0;
// Flatten recursion if preferred, by removing consecutive frames
// sharing the same location.
if (options.flattenRecursion) {
frames = frames.filter(this._isConsecutiveDuplicate);
}
// Apply a provided filter function. This can be used, for example, to
// filter out platform frames if only content-related function calls
// should be taken into consideration.
if (options.filterFrames) {
frames = frames.filter(options.filterFrames);
}
// If no frames are available, add a pseudo "idle" block in between.
if (options.showIdleBlocks && frames.length == 0) {
frames = [{ location: options.showIdleBlocks || "" }];
}
for (let { location } of frames) {
let prevFrame = prevFrames[frameIndex];
@ -894,6 +920,22 @@ let FlameGraphUtils = {
return out;
},
/**
* Checks if the provided frame is the same as the next one in a sample.
*
* @param object e
* An object containing a { location } property.
* @param number index
* The index of the object in the parent array.
* @param array array
* The parent array.
* @return boolean
* True if the next frame shares the same location, false otherwise.
*/
_isConsecutiveDuplicate: function(e, index, array) {
return index < array.length - 1 && e.location != array[index + 1].location;
},
/**
* Very dumb hashing of a string. Used to pick colors from a pallette.
*

View File

@ -2426,10 +2426,27 @@ Variable.prototype = Heritage.extend(Scope.prototype, {
this._valueLabel.classList.remove(VariablesView.getClass(prevGrip));
}
this._valueGrip = aGrip;
this._valueString = VariablesView.getString(aGrip, {
concise: true,
noEllipsis: true,
});
if(aGrip && (aGrip.optimizedOut || aGrip.uninitialized || aGrip.missingArguments)) {
if(aGrip.optimizedOut) {
this._valueString = STR.GetStringFromName("variablesViewOptimizedOut")
}
else if(aGrip.uninitialized) {
this._valueString = STR.GetStringFromName("variablesViewUninitialized")
}
else if(aGrip.missingArguments) {
this._valueString = STR.GetStringFromName("variablesViewMissingArgs")
}
this.eval = null;
}
else {
this._valueString = VariablesView.getString(aGrip, {
concise: true,
noEllipsis: true,
});
this.eval = this.ownerView.eval;
}
this._valueClassName = VariablesView.getClass(aGrip);
this._valueLabel.classList.add(this._valueClassName);

View File

@ -316,4 +316,8 @@ functionSearchSeparatorLabel=←
# resumed first.
resumptionOrderPanelTitle=There are one or more paused debuggers. Please resume the most-recently paused debugger first at: %S
variablesViewOptimizedOut=(optimized away)
variablesViewUninitialized=(uninitialized)
variablesViewMissingArgs=(unavailable)
evalGroupLabel=Evaluated Sources

View File

@ -87,6 +87,10 @@ category.events=Input & Events
# This string is displayed in the call tree for the root node.
table.root=(root)
# LOCALIZATION NOTE (table.idle):
# This string is displayed in the call tree for the idle blocks.
table.idle=(idle)
# LOCALIZATION NOTE (table.url.tooltiptext):
# This string is displayed in the call tree as the tooltip text for the url
# labels which, when clicked, jump to the debugger.

View File

@ -13,30 +13,8 @@ clientShortname2=Firefox Hello
first_time_experience_title={{clientShortname}} — Join the conversation
first_time_experience_button_label=Get Started
share_link_header_text=Share this link to invite someone to talk:
invite_header_text=Invite someone to join you.
## LOCALIZATION NOTE(invitee_name_label): Displayed when obtaining a url.
## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#precall-firstrun
## Click the label icon at the end of the url field.
invitee_name_label=Who are you inviting?
## LOCALIZATION NOTE(invitee_expire_days_label): Allows the user to adjust
## the expiry time. Click the label icon at the end of the url field to see where
## this is:
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#precall-firstrun
## Semicolon-separated list of plural forms. See:
## http://developer.mozilla.org/en/docs/Localization_and_Plurals
## In this item, don't translate the part between {{..}}
invitee_expire_days_label=Invitation will expire in {{expiry_time}} day;Invitation will expire in {{expiry_time}} days
## LOCALIZATION NOTE(invitee_expire_hours_label): Allows the user to adjust
## the expiry time. Click the label icon are the end of the url field to see where
## this is:
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#precall-firstrun
## Semicolon-separated list of plural forms. See:
## http://developer.mozilla.org/en/docs/Localization_and_Plurals
## In this item, don't translate the part between {{..}}
invitee_expire_hours_label=Invitation will expire in {{expiry_time}} hour;Invitation will expire in {{expiry_time}} hours
# Status text
display_name_guest=Guest
display_name_dnd_status=Do Not Disturb
@ -66,9 +44,7 @@ share_email_subject4={{clientShortname}} — Join the conversation
## LOCALIZATION NOTE (share_email_body4): In this item, don't translate the
## part between {{..}} and leave the \r\n\r\n part alone
share_email_body4=Hello!\r\n\r\nJoin me for a video conversation using {{clientShortname}}:\r\n\r\nYou don't have to download or install anything. Just copy and paste this URL into your browser:\r\n\r\n{{callUrl}}\r\n\r\nIf you want, you can also learn more about {{clientShortname}} at {{learnMoreUrl}}\r\n\r\nTalk to you soon!
share_button=Email
share_button2=Email Link
copy_url_button=Copy
copy_url_button2=Copy Link
copied_url_button=Copied!
@ -120,6 +96,13 @@ add_or_import_contact_title=Add or Import Contact
## for where these appear on the UI
import_contacts_button=Import
importing_contacts_progress_button=Importing…
import_contacts_failure_message=Some contacts could not be imported. Please try again.
## LOCALIZATION NOTE(import_contacts_success_message): Success notification message
## when user's contacts have been successfully imported.
## Semicolon-separated list of plural forms. See:
## http://developer.mozilla.org/en/docs/Localization_and_Plurals
## In this item, don't translate the part between {{..}}
import_contacts_success_message={{total}} contact was successfully imported.;{{total}} contacts were successfully imported.
## LOCALIZATION NOTE(sync_contacts_button): This button is displayed in place of
## importing_contacts_button once contacts have been imported once.
sync_contacts_button=Sync Contacts

View File

@ -27,13 +27,21 @@ this.ContentWebRTC = {
Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false);
},
// Called only for 'unload' to remove pending gUM prompts in reloaded frames.
handleEvent: function(aEvent) {
let contentWindow = aEvent.target.defaultView;
let mm = getMessageManagerForWindow(contentWindow);
for (let key of contentWindow.pendingGetUserMediaRequests.keys())
mm.sendAsyncMessage("webrtc:CancelRequest", key);
},
receiveMessage: function(aMessage) {
switch (aMessage.name) {
case "webrtc:Allow":
let callID = aMessage.data.callID;
let contentWindow = Services.wm.getOuterWindowWithId(aMessage.data.windowID);
let devices = contentWindow.pendingGetUserMediaRequests.get(callID);
contentWindow.pendingGetUserMediaRequests.delete(callID);
forgetRequest(contentWindow, callID);
let allowedDevices = Cc["@mozilla.org/supports-array;1"]
.createInstance(Ci.nsISupportsArray);
@ -112,8 +120,10 @@ function prompt(aContentWindow, aWindowID, aCallID, aConstraints, aDevices, aSec
return;
}
if (!aContentWindow.pendingGetUserMediaRequests)
if (!aContentWindow.pendingGetUserMediaRequests) {
aContentWindow.pendingGetUserMediaRequests = new Map();
aContentWindow.addEventListener("unload", ContentWebRTC);
}
aContentWindow.pendingGetUserMediaRequests.set(aCallID, devices);
let request = {
@ -143,9 +153,17 @@ function denyRequest(aData, aError) {
return;
let contentWindow = Services.wm.getOuterWindowWithId(aData.windowID);
if (contentWindow.pendingGetUserMediaRequests)
contentWindow.pendingGetUserMediaRequests.delete(aData.callID);
forgetRequest(contentWindow, aData.callID);
}
function forgetRequest(aContentWindow, aCallID) {
aContentWindow.pendingGetUserMediaRequests.delete(aCallID);
if (aContentWindow.pendingGetUserMediaRequests.size)
return;
aContentWindow.removeEventListener("unload", ContentWebRTC);
aContentWindow.pendingGetUserMediaRequests = null;
}
function updateIndicators() {
let contentWindowSupportsArray = MediaManagerService.activeMediaCaptureWindows;

View File

@ -28,6 +28,7 @@ this.webrtcUI = {
let mm = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
mm.addMessageListener("webrtc:Request", this);
mm.addMessageListener("webrtc:CancelRequest", this);
mm.addMessageListener("webrtc:UpdateBrowserIndicators", this);
},
@ -42,6 +43,7 @@ this.webrtcUI = {
let mm = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
mm.removeMessageListener("webrtc:Request", this);
mm.removeMessageListener("webrtc:CancelRequest", this);
mm.removeMessageListener("webrtc:UpdateBrowserIndicators", this);
},
@ -125,6 +127,9 @@ this.webrtcUI = {
case "webrtc:Request":
prompt(aMessage.target, aMessage.data);
break;
case "webrtc:CancelRequest":
removePrompt(aMessage.target, aMessage.data);
break;
case "webrtc:UpdatingIndicators":
webrtcUI._streams = [];
break;
@ -166,7 +171,9 @@ function getHost(uri, href) {
host = uri.specIgnoringRef;
} else {
// This is unfortunate, but we should display *something*...
host = bundle.getString("getUserMedia.sharingMenuUnknownHost");
const kBundleURI = "chrome://browser/locale/browser.properties";
let bundle = Services.strings.createBundle(kBundleURI);
host = bundle.GetStringFromName("getUserMedia.sharingMenuUnknownHost");
}
}
return host;
@ -435,6 +442,15 @@ function prompt(aBrowser, aRequest) {
chromeWin.PopupNotifications.show(aBrowser, "webRTC-shareDevices", message,
anchorId, mainAction, secondaryActions,
options);
notification.callID = aRequest.callID;
}
function removePrompt(aBrowser, aCallId) {
let chromeWin = aBrowser.ownerDocument.defaultView;
let notification =
chromeWin.PopupNotifications.getNotification("webRTC-shareDevices", aBrowser);
if (notification && notification.callID == aCallId)
notification.remove();
}
function getGlobalIndicator() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 B

View File

@ -77,7 +77,7 @@
</style>
<style name="GeckoActionBar" parent="@android:style/Widget.Holo.Light.ActionMode">
<item name="android:background">@drawable/ab_stacked_transparent_light_holo</item>
<item name="android:background">@drawable/ab_background</item>
</style>
<style name="TextAppearance.Widget.ActionBar.Title" parent="@android:style/TextAppearance.Medium"/>

View File

@ -751,7 +751,7 @@
</style>
<style name="GeckoActionBar">
<item name="android:background">@drawable/ab_stacked_transparent_light_holo</item>
<item name="android:background">@drawable/ab_background</item>
</style>
<style name="GeckoActionBar.Title">

View File

@ -46,6 +46,8 @@ Cu.import("resource://gre/modules/Services.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gDebug",
@ -81,6 +83,102 @@ Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() {
DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
}, false);
/**
* A set of Promise that supports waiting.
*
* Promise items may be added or removed during the wait. The wait will
* resolve once all Promise items have been resolved or removed.
*/
function PromiseSet() {
/**
* key: the Promise passed pass the client of the `PromiseSet`.
* value: an indirection on top of `key`, as an object with
* the following fields:
* - indirection: a Promise resolved if `key` is resolved or
* if `resolve` is called
* - resolve: a function used to resolve the indirection.
*/
this._indirections = new Map();
}
PromiseSet.prototype = {
/**
* Wait until all Promise have been resolved or removed.
*
* Note that calling `wait()` causes Promise to be removed from the
* Set once they are resolved.
*
* @return {Promise} Resolved once all Promise have been resolved or removed,
* or rejected after at least one Promise has rejected.
*/
wait: function() {
// Pick an arbitrary element in the map, if any exists.
let entry = this._indirections.entries().next();
if (entry.done) {
// No indirections left, we are done.
return Promise.resolve();
}
let [, indirection] = entry.value;
let promise = indirection.promise;
promise = promise.then(() =>
// At this stage, the entry has been cleaned up.
this.wait()
);
return promise;
},
/**
* Add a new Promise to the set.
*
* Calls to wait (including ongoing calls) will only return once
* `key` has either resolved or been removed.
*/
add: function(key) {
this._ensurePromise(key);
let indirection = PromiseUtils.defer();
key.then(
x => {
// Clean up immediately.
// This needs to be done before the call to `resolve`, otherwise
// `wait()` may loop forever.
this._indirections.delete(key);
indirection.resolve(x);
},
err => {
this._indirections.delete(key);
indirection.reject(err);
});
this._indirections.set(key, indirection);
},
/**
* Remove a Promise from the set.
*
* Calls to wait (including ongoing calls) will ignore this promise,
* unless it is added again.
*/
delete: function(key) {
this._ensurePromise(key);
let value = this._indirections.get(key);
if (!value) {
return false;
}
this._indirections.delete(key);
value.resolve();
return true;
},
_ensurePromise: function(key) {
if (!key || typeof key != "object") {
throw new Error("Expected an object");
}
if ((!"then" in key) || typeof key.then != "function") {
throw new Error("Expected a Promise");
}
},
};
/**
* Display a warning.
@ -391,23 +489,44 @@ function Barrier(name) {
throw new TypeError("Instances of Barrier need a (non-empty) name");
}
/**
* The set of conditions registered by clients, as a map.
*
* Key: condition (function)
* Value: Array of {name: string, fetchState: function, filename: string,
* lineNumber: number, stack: string}
*/
this._conditions = new Map();
/**
* Indirections, used to let clients cancel a blocker when they
* call removeBlocker().
* The set of all Promise for which we need to wait before the barrier
* is lifted. Note that this set may be changed while we are waiting.
*
* Key: condition (function)
* Value: Deferred.
* Set to `null` once the wait is complete.
*/
this._indirections = null;
this._waitForMe = new PromiseSet();
/**
* A map from conditions, as passed by users during the call to `addBlocker`,
* to `promise`, as present in `this._waitForMe`.
*
* Used to let users perform cleanup through `removeBlocker`.
* Set to `null` once the wait is complete.
*
* Key: condition (any, as passed by user)
* Value: promise used as a key in `this._waitForMe`. Note that there is
* no guarantee that the key is still present in `this._waitForMe`.
*/
this._conditionToPromise = new Map();
/**
* A map from Promise, as present in `this._waitForMe` or
* `this._conditionToPromise`, to information on blockers.
*
* Key: Promise (as present in this._waitForMe or this._conditionToPromise).
* Value: {
* trigger: function,
* promise,
* name,
* fetchState: function,
* stack,
* filename,
* lineNumber
* };
*/
this._promiseToBlocker = new Map();
/**
* The name of the barrier.
@ -420,9 +539,9 @@ function Barrier(name) {
this._promise = null;
/**
* An array of objects used to monitor the state of each blocker.
* `true` once we have started waiting.
*/
this._monitors = null;
this._isStarted = false;
/**
* The capability of adding blockers. This object may safely be returned
@ -465,7 +584,7 @@ function Barrier(name) {
* - filename A string containing the filename for the caller. This
* module can generally infer the information if it is not provided.
*/
addBlocker: function(name, condition, details) {
addBlocker: (name, condition, details) => {
if (typeof name != "string") {
throw new TypeError("Expected a human-readable name as first argument");
}
@ -479,12 +598,12 @@ function Barrier(name) {
if (typeof details != "object") {
throw new TypeError("Expected an object as third argument to `addBlocker`, got " + details);
}
if (!this._conditions) {
throw new Error("Phase " + this._name +
" has already begun, it is too late to register" +
" completion condition '" + name + "'.");
if (!this._waitForMe) {
throw new Error(`Phase "${ this._name } is finished, it is too late to register completion condition "${ name }"`);
}
// Normalize the details
let fetchState = details.fetchState || null;
let filename = details.filename || "?";
let lineNumber = details.lineNumber || -1;
@ -517,17 +636,73 @@ function Barrier(name) {
}
}
let set = this._conditions.get(condition);
if (!set) {
set = [];
this._conditions.set(condition, set);
// Split the condition between a trigger function and a promise.
// The function to call to notify the blocker that we have started waiting.
// This function returns a promise resolved/rejected once the
// condition is complete, and never throws.
let trigger;
// A promise resolved once the condition is complete.
let promise;
if (typeof condition == "function") {
promise = new Promise((resolve, reject) => {
trigger = () => {
try {
resolve(condition());
} catch (ex) {
reject(ex);
}
}
});
} else {
// If `condition` is not a function, `trigger` is not particularly
// interesting, and `condition` needs to be normalized to a promise.
trigger = () => {};
promise = Promise.resolve(condition);
}
set.push({name: name,
fetchState: fetchState,
filename: filename,
lineNumber: lineNumber,
stack: stack});
}.bind(this),
// Make sure that `promise` never rejects.
promise = promise.then(null, error => {
let msg = `A blocker encountered an error while we were waiting.
Blocker: ${ name }
Phase: ${ this._name }
State: ${ safeGetState(fetchState) }`;
warn(msg, error);
// The error should remain uncaught, to ensure that it
// still causes tests to fail.
Promise.reject(error);
});
let blocker = {
trigger: trigger,
promise: promise,
name: name,
fetchState: fetchState,
stack: stack,
filename: filename,
lineNumber: lineNumber
};
this._waitForMe.add(promise);
this._promiseToBlocker.set(promise, blocker);
this._conditionToPromise.set(condition, promise);
// As conditions may hold lots of memory, we attempt to cleanup
// as soon as we are done (which might be in the next tick, if
// we have been passed a resolved promise).
promise = promise.then(() =>
this._removeBlocker(condition)
);
if (this._isStarted) {
// The wait has already started. The blocker should be
// notified asap. We do it out of band as clients probably
// expect `addBlocker` to return immediately.
Promise.resolve().then(trigger);
}
},
/**
* Remove the blocker for a condition.
@ -539,24 +714,9 @@ function Barrier(name) {
* @return {boolean} true if at least one blocker has been
* removed, false otherwise.
*/
removeBlocker: function(condition) {
if (this._conditions) {
// wait() hasn't been called yet.
return this._conditions.delete(condition);
}
if (this._indirections) {
// wait() is in progress
let deferred = this._indirections.get(condition);
if (deferred) {
// Unlock the blocker
deferred.resolve();
}
return this._indirections.delete(condition);
}
// wait() is complete.
return false;
}.bind(this),
removeBlocker: (condition) => {
return this._removeBlocker(condition);
}
};
}
Barrier.prototype = Object.freeze({
@ -565,21 +725,21 @@ Barrier.prototype = Object.freeze({
* designed for error-reporting.
*/
get state() {
if (this._conditions) {
if (!this._isStarted) {
return "Not started";
}
if (!this._monitors) {
if (!this._waitForMe) {
return "Complete";
}
let frozen = [];
for (let {name, isComplete, fetchState, stack, filename, lineNumber} of this._monitors) {
if (!isComplete) {
frozen.push({name: name,
state: safeGetState(fetchState),
filename: filename,
lineNumber: lineNumber,
stack: stack});
}
for (let {name, fetchState, stack, filename, lineNumber} of this._promiseToBlocker.values()) {
frozen.push({
name: name,
state: safeGetState(fetchState),
filename: filename,
lineNumber: lineNumber,
stack: stack
});
}
return frozen;
},
@ -614,109 +774,43 @@ Barrier.prototype = Object.freeze({
return this._promise = this._wait(options);
},
_wait: function(options) {
// Sanity checks
if (this._isStarted) {
throw new TypeError("Internal error: already started " + this._name);
}
if (!this._waitForMe || !this._conditionToPromise || !this._promiseToBlocker) {
throw new TypeError("Internal error: already finished " + this._name);
}
let topic = this._name;
let conditions = this._conditions;
this._conditions = null; // Too late to register
if (conditions.size == 0) {
return Promise.resolve();
// Notify blockers
for (let blocker of this._promiseToBlocker.values()) {
blocker.trigger(); // We have guarantees that this method will never throw
}
this._indirections = new Map();
// The promises for which we are waiting.
let allPromises = [];
this._isStarted = true;
// Information to determine and report to the user which conditions
// are not satisfied yet.
this._monitors = [];
for (let _condition of conditions.keys()) {
for (let current of conditions.get(_condition)) {
let condition = _condition; // Avoid capturing the wrong variable
let {name, fetchState, stack, filename, lineNumber} = current;
// An indirection on top of condition, used to let clients
// cancel a blocker through removeBlocker.
let indirection = Promise.defer();
this._indirections.set(condition, indirection);
// Gather all completion conditions
try {
if (typeof condition == "function") {
// Normalize |condition| to the result of the function.
try {
condition = condition(topic);
} catch (ex) {
condition = Promise.reject(ex);
}
}
// Normalize to a promise. Of course, if |condition| was not a
// promise in the first place (in particular if the above
// function returned |undefined| or failed), that new promise
// isn't going to be terribly interesting, but it will behave
// as a promise.
condition = Promise.resolve(condition);
let monitor = {
isComplete: false,
name: name,
fetchState: fetchState,
stack: stack,
filename: filename,
lineNumber: lineNumber
};
condition = condition.then(null, function onError(error) {
let msg = "A completion condition encountered an error" +
" while we were spinning the event loop." +
" Condition: " + name +
" Phase: " + topic +
" State: " + safeGetState(fetchState);
warn(msg, error);
// The error should remain uncaught, to ensure that it
// still causes tests to fail.
Promise.reject(error);
});
condition.then(() => indirection.resolve());
indirection.promise.then(() => monitor.isComplete = true);
this._monitors.push(monitor);
allPromises.push(indirection.promise);
} catch (error) {
let msg = "A completion condition encountered an error" +
" while we were initializing the phase." +
" Condition: " + name +
" Phase: " + topic +
" State: " + safeGetState(fetchState);
warn(msg, error);
}
}
}
conditions = null;
let promise = Promise.all(allPromises);
allPromises = null;
// Now, wait
let promise = this._waitForMe.wait();
promise = promise.then(null, function onError(error) {
// I don't think that this can happen.
// However, let's be overcautious with async/shutdown error reporting.
let msg = "An uncaught error appeared while completing the phase." +
" Phase: " + topic;
" Phase: " + topic;
warn(msg, error);
});
promise = promise.then(() => {
this._monitors = null;
this._indirections = null;
}); // Memory cleanup
// Cleanup memory
this._waitForMe = null;
this._promiseToBlocker = null;
this._conditionToPromise = null;
});
// Now handle warnings and crashes
let warnAfterMS = DELAY_WARNING_MS;
if (options && "warnAfterMS" in options) {
if (typeof options.warnAfterMS == "number"
@ -732,12 +826,12 @@ Barrier.prototype = Object.freeze({
// If the promise takes too long to be resolved/rejected,
// we need to notify the user.
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(function() {
timer.initWithCallback(() => {
let msg = "At least one completion condition is taking too long to complete." +
" Conditions: " + JSON.stringify(this.state) +
" Barrier: " + topic;
" Conditions: " + JSON.stringify(this.state) +
" Barrier: " + topic;
warn(msg);
}.bind(this), warnAfterMS, Ci.nsITimer.TYPE_ONE_SHOT);
}, warnAfterMS, Ci.nsITimer.TYPE_ONE_SHOT);
promise = promise.then(function onSuccess() {
timer.cancel();
@ -769,8 +863,8 @@ Barrier.prototype = Object.freeze({
timeToCrash = looseTimer(crashAfterMS);
timeToCrash.promise.then(
function onTimeout() {
// Report the problem as best as we can, then crash.
let state = this.state;
// Report the problem as best as we can, then crash.
let state = this.state;
// If you change the following message, please make sure
// that any information on the topic and state appears
@ -779,42 +873,39 @@ Barrier.prototype = Object.freeze({
let msg = "AsyncShutdown timeout in " + topic +
" Conditions: " + JSON.stringify(state) +
" At least one completion condition failed to complete" +
" within a reasonable amount of time. Causing a crash to" +
" ensure that we do not leave the user with an unresponsive" +
" process draining resources.";
fatalerr(msg);
if (gCrashReporter && gCrashReporter.enabled) {
" within a reasonable amount of time. Causing a crash to" +
" ensure that we do not leave the user with an unresponsive" +
" process draining resources.";
fatalerr(msg);
if (gCrashReporter && gCrashReporter.enabled) {
let data = {
phase: topic,
conditions: state
};
};
gCrashReporter.annotateCrashReport("AsyncShutdownTimeout",
JSON.stringify(data));
} else {
} else {
warn("No crash reporter available");
}
}
// To help sorting out bugs, we want to make sure that the
// call to nsIDebug.abort points to a guilty client, rather
// than to AsyncShutdown itself. We search through all the
// clients until we find one that is guilty and use its
// filename/lineNumber, which have been determined during
// the call to `addBlocker`.
// than to AsyncShutdown itself. We pick a client that is
// still blocking and use its filename/lineNumber,
// which have been determined during the call to `addBlocker`.
let filename = "?";
let lineNumber = -1;
for (let monitor of this._monitors) {
if (monitor.isComplete) {
continue;
}
filename = monitor.filename;
lineNumber = monitor.lineNumber;
for (let blocker of this._promiseToBlocker) {
filename = blocker.filename;
lineNumber = blocker.lineNumber;
break;
}
gDebug.abort(filename, lineNumber);
gDebug.abort(filename, lineNumber);
}.bind(this),
function onSatisfied() {
// The promise has been rejected, which means that we have satisfied
// all completion conditions.
});
function onSatisfied() {
// The promise has been rejected, which means that we have satisfied
// all completion conditions.
});
promise = promise.then(function() {
timeToCrash.reject();
@ -823,6 +914,23 @@ Barrier.prototype = Object.freeze({
return promise;
},
_removeBlocker: function(condition) {
if (!this._waitForMe || !this._promiseToBlocker || !this._conditionToPromise) {
// We have already cleaned up everything.
return false;
}
let promise = this._conditionToPromise.get(condition);
if (!promise) {
// The blocker has already been removed
return false;
}
this._conditionToPromise.delete(condition);
this._promiseToBlocker.delete(promise);
return this._waitForMe.delete(promise);
},
});

View File

@ -2,6 +2,8 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
function run_test() {
run_next_test();
}
@ -15,30 +17,71 @@ add_task(function* test_no_condition() {
}
});
add_task(function* test_phase_various_failures() {
do_print("Ensure that we cannot add a condition for a phase once notification has been received");
for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
do_print("Kind: " + kind);
// Testing with wrong arguments
let lock = makeLock(kind);
lock.wait(); // Don't actually wait for the promise to be resolved
let exn = get_exn(() => lock.addBlocker("Test", true));
do_check_true(!!exn);
if (kind == "xpcom-barrier") {
do_print("Skipping this part of the test that is caught differently by XPConnect");
continue;
Assert.throws(() => lock.addBlocker(), /TypeError|NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS/);
Assert.throws(() => lock.addBlocker(null, true), /TypeError|NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS/);
if (kind != "xpcom-barrier") {
// xpcom-barrier actually expects a string in that position
Assert.throws(() => lock.addBlocker("Test 2", () => true, "not a function"), /TypeError/);
}
do_print("Ensure that an incomplete blocker causes a TypeError");
lock = makeLock(kind);
exn = get_exn(() => lock.addBlocker());
do_check_exn(exn, "TypeError");
// Attempting to add a blocker after we are done waiting
yield lock.wait();
Assert.throws(() => lock.addBlocker("Test 3", () => true), /is finished/);
}
});
exn = get_exn(() => lock.addBlocker(null, true));
do_check_exn(exn, "TypeError");
add_task(function* test_reentrant() {
do_print("Ensure that we can call addBlocker from within a blocker");
exn = get_exn(() => lock.addBlocker("Test 2", () => true, "not a function"));
do_check_exn(exn, "TypeError");
for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
do_print("Kind: " + kind);
let lock = makeLock(kind);
let deferredOuter = PromiseUtils.defer();
let deferredInner = PromiseUtils.defer();
let deferredBlockInner = PromiseUtils.defer();
lock.addBlocker("Outer blocker", () => {
do_print("Entering outer blocker");
deferredOuter.resolve();
lock.addBlocker("Inner blocker", () => {
do_print("Entering inner blocker");
deferredInner.resolve();
return deferredBlockInner.promise;
});
});
// Note that phase-style locks spin the event loop and do not return from
// `lock.wait()` until after all blockers have been resolved. Therefore,
// to be able to test them, we need to dispatch the following steps to the
// event loop before calling `lock.wait()`, which we do by forcing
// a Promise.resolve().
//
let promiseSteps = Task.spawn(function* () {
yield Promise.resolve();
do_print("Waiting until we have entered the outer blocker");
yield deferredOuter.promise;
do_print("Waiting until we have entered the inner blocker");
yield deferredInner.promise;
do_print("Allowing the lock to resolve")
deferredBlockInner.resolve();
});
do_print("Starting wait");
yield lock.wait();
do_print("Waiting until all steps have been walked");
yield promiseSteps;
}
});
@ -149,4 +192,3 @@ add_task(function* test_state() {
add_task(function*() {
Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
});

View File

@ -662,7 +662,12 @@ this.DownloadIntegration = {
// We should report errors with making the permissions less restrictive
// or marking the file as read-only on Unix and Mac, but this should not
// prevent the download from completing.
Cu.reportError(ex);
// The setPermissions API error EPERM is expected to occur when working
// on a file system that does not support file permissions, like FAT32,
// thus we don't report this error.
if (!(ex instanceof OS.File.Error) || ex.unixErrno != OS.Constants.libc.EPERM) {
Cu.reportError(ex);
}
}
gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url),

View File

@ -76,7 +76,7 @@ LoginManager.prototype = {
},
/* ---------- private memebers ---------- */
/* ---------- private members ---------- */
__formFillService : null, // FormFillController, for username autocompleting
@ -130,6 +130,8 @@ LoginManager.prototype = {
// Initialize storage so that asynchronous data loading can start.
this._initStorage();
}
Services.obs.addObserver(this._observer, "gather-telemetry", false);
},
@ -198,12 +200,20 @@ LoginManager.prototype = {
Services.obs.notifyObservers(null,
"passwordmgr-storage-replace-complete", null);
}.bind(this));
} else if (topic == "gather-telemetry") {
this._pwmgr._gatherTelemetry();
} else {
log("Oops! Unexpected notification:", topic);
}
}
},
_gatherTelemetry : function() {
let numPasswordsHist = Services.telemetry.getHistogramById("PWMGR_NUM_SAVED_PASSWORDS");
numPasswordsHist.clear();
numPasswordsHist.add(this.countLogins("", "", ""));
},

View File

@ -29,9 +29,10 @@ SuggestAutoComplete.prototype = {
},
get _suggestionLabel() {
delete this._suggestionLabel;
let bundle = Services.strings.createBundle("chrome://global/locale/search/search.properties");
return this._suggestionLabel = bundle.GetStringFromName("suggestion_label");
let label = bundle.GetStringFromName("suggestion_label");
Object.defineProperty(SuggestAutoComplete.prototype, "_suggestionLabel", {value: label});
return label;
},
/**

View File

@ -7149,5 +7149,13 @@
"high": "1024",
"n_buckets": 16,
"description": "The number of entries in persistent DataStorage (HSTS and HPKP data, basically)"
},
"PWMGR_NUM_SAVED_PASSWORDS": {
"expires_in_version": "never",
"kind": "exponential",
"high": 750,
"n_buckets" : 50,
"extended_statistics_ok": true,
"description": "The number of saved signons in storage"
}
}

View File

@ -1701,6 +1701,18 @@ ThreadActor.prototype = {
if (aValue === null) {
return { type: "null" };
}
else if(aValue.optimizedOut ||
aValue.uninitialized ||
aValue.missingArguments) {
// The slot is optimized out, an uninitialized binding, or
// arguments on a dead scope
return {
type: "null",
optimizedOut: aValue.optimizedOut,
uninitialized: aValue.uninitialized,
missingArguments: aValue.missingArguments
};
}
return this.objectGrip(aValue, aPool);
case "symbol":
@ -4864,20 +4876,14 @@ EnvironmentActor.prototype = {
}
for each (let name in parameterNames) {
let arg = {};
let value = this.obj.getVariable(name);
// The slot is optimized out.
// FIXME: Need actual UI, bug 941287.
if (value && value.optimizedOut) {
continue;
}
// TODO: this part should be removed in favor of the commented-out part
// below when getVariableDescriptor lands (bug 725815).
let desc = {
value: value,
configurable: false,
writable: true,
writable: !(value && value.optimizedOut),
enumerable: true
};
@ -4905,19 +4911,16 @@ EnvironmentActor.prototype = {
}
let value = this.obj.getVariable(name);
// The slot is optimized out, arguments on a dead scope, or an
// uninitialized binding.
// FIXME: Need actual UI, bug 941287.
if (value && (value.optimizedOut || value.missingArguments || value.uninitialized)) {
continue;
}
// TODO: this part should be removed in favor of the commented-out part
// below when getVariableDescriptor lands.
let desc = {
value: value,
configurable: false,
writable: true,
writable: !(value &&
(value.optimizedOut ||
value.uninitialized ||
value.missingArguments)),
enumerable: true
};

Some files were not shown because too many files have changed in this diff Show More