Merge m-c to b2g-inbound a=merge

This commit is contained in:
Wes Kocher 2015-01-16 18:07:28 -08:00
commit 7c3409c078
509 changed files with 9271 additions and 5189 deletions

View File

@ -22,4 +22,4 @@
# changes to stick? As of bug 928195, this shouldn't be necessary! Please
# don't change CLOBBER for WebIDL changes any more.
Bug 1101553 - remove nsPIPlacesHistoryListenersNotifier
Bug 1118618's backout needed a clobber

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

@ -3,6 +3,8 @@ const EXPECTED_CHAIN = [
"MIIC2jCCAcKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwHhcNMTQwOTI1MjEyMTU0WhcNMjQwOTI1MjEyMTU0WjAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBT+BwAhO52IWgSIdZZifU9LHOs3IR/+8DCC0WP5d/OuyKlZ6Rqd0tsd3i7durhQyjHSbLf2lJStcnFjcVEbEnNI76RuvlN8xLLn5eV+2Ayr4cZYKztudwRmw+DV/iYAiMSy0hs7m3ssfX7qpoi1aNRjUanwU0VTCPQhF1bEKAC2du+C5Z8e92zN5t87w7bYr7lt+m8197XliXEu+0s9RgnGwGaZ296BIRz6NOoJYTa43n06LU1I1+Z4d6lPdzUFrSR0GBaMhUSurUBtOin3yWiMhg1VHX/KwqGc4als5GyCVXy8HGrA/0zQPOhetxrlhEVAdK/xBt7CZvByj1Rcc7AgMBAAGjEzARMA8GA1UdEwQIMAYBAf8CAQAwDQYJKoZIhvcNAQELBQADggEBAJq/hogSRqzPWTwX4wTn/DVSNdWwFLv53qep9YrSMJ8ZsfbfK9Es4VP4dBLRQAVMJ0Z5mW1I6d/n0KayTanuUBvemYdxPi/qQNSs8UJcllqdhqWzmzAg6a0LxrMnEeKzPBPD6q8PwQ7tYP+B4sBN9tnnsnyPgti9ZiNZn5FwXZliHXseQ7FE9/SqHlLw5LXW3YtKjuti6RmuV6fq3j+D4oeC5vb1mKgIyoTqGN6ze57v8RHi+pQ8Q+kmoUn/L3Z2YmFe4SKN/4WoyXr8TdejpThGOCGCAd3565s5gOx5QfSQX11P8NZKO8hcN0tme3VzmGpHK0Z/6MTmdpNaTwQ6odk="
];
const MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE = -16384;
function parseReport(request) {
// read the report from the request
let inputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);
@ -38,6 +40,12 @@ function handleRequest(request, response) {
}
}
if (report.errorCode !== MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE) {
response.setStatusLine("1.1", 500, "Server error");
response.write("<html>The report contained an unexpected error code</html>");
return;
}
// if all is as expected, send the 201 the client expects
response.setStatusLine("1.1", 201, "Created");
response.write("<html>OK</html>");

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() {

View File

@ -183,7 +183,7 @@ Attr::GetValue(nsAString& aValue)
{
Element* element = GetElement();
if (element) {
nsCOMPtr<nsIAtom> nameAtom = GetNameAtom(element);
nsCOMPtr<nsIAtom> nameAtom = mNodeInfo->NameAtom();
element->GetAttr(mNodeInfo->NamespaceID(), nameAtom, aValue);
}
else {
@ -202,7 +202,7 @@ Attr::SetValue(const nsAString& aValue, ErrorResult& aRv)
return;
}
nsCOMPtr<nsIAtom> nameAtom = GetNameAtom(element);
nsCOMPtr<nsIAtom> nameAtom = mNodeInfo->NameAtom();
aRv = element->SetAttr(mNodeInfo->NamespaceID(),
nameAtom,
mNodeInfo->GetPrefixAtom(),

View File

@ -1199,7 +1199,9 @@ Element::RemoveAttributeNode(Attr& aAttribute,
ErrorResult& aError)
{
OwnerDoc()->WarnOnceAbout(nsIDocument::eRemoveAttributeNode);
return Attributes()->RemoveNamedItem(aAttribute.NodeName(), aError);
nsAutoString nameSpaceURI;
aAttribute.NodeInfo()->GetNamespaceURI(nameSpaceURI);
return Attributes()->RemoveNamedItemNS(nameSpaceURI, aAttribute.NodeInfo()->LocalName(), aError);
}
void

View File

@ -474,7 +474,7 @@ RemotePermissionRequest::DoAllow(JS::HandleValue aChoices)
// PContentPermissionRequestChild
bool
RemotePermissionRequest::Recv__delete__(const bool& aAllow,
const nsTArray<PermissionChoice>& aChoices)
InfallibleTArray<PermissionChoice>&& aChoices)
{
if (aAllow && mWindow->IsCurrentInnerWindow()) {
// Use 'undefined' if no choice is provided.

View File

@ -117,7 +117,7 @@ public:
// It will be called when prompt dismissed.
virtual bool Recv__delete__(const bool &aAllow,
const nsTArray<PermissionChoice>& aChoices) MOZ_OVERRIDE;
InfallibleTArray<PermissionChoice>&& aChoices) MOZ_OVERRIDE;
void IPDLAddRef()
{
@ -146,3 +146,4 @@ private:
};
#endif // nsContentPermissionHelper_h

View File

@ -316,46 +316,51 @@ nsDOMAttributeMap::SetNamedItemInternal(Attr& aAttr,
}
// Get nodeinfo and preexisting attribute (if it exists)
nsAutoString name;
nsRefPtr<mozilla::dom::NodeInfo> ni;
nsRefPtr<NodeInfo> oldNi;
if (!aWithNS) {
nsAutoString name;
aAttr.GetName(name);
oldNi = mContent->GetExistingAttrNameFromQName(name);
}
else {
uint32_t i, count = mContent->GetAttrCount();
for (i = 0; i < count; ++i) {
const nsAttrName* name = mContent->GetAttrNameAt(i);
int32_t attrNS = name->NamespaceID();
nsIAtom* nameAtom = name->LocalName();
// we're purposefully ignoring the prefix.
if (aAttr.NodeInfo()->Equals(nameAtom, attrNS)) {
oldNi = mContent->NodeInfo()->NodeInfoManager()->
GetNodeInfo(nameAtom, name->GetPrefix(), aAttr.NodeInfo()->NamespaceID(),
nsIDOMNode::ATTRIBUTE_NODE);
break;
}
}
}
nsRefPtr<Attr> attr;
// SetNamedItemNS()
if (aWithNS) {
// Return existing attribute, if present
ni = aAttr.NodeInfo();
if (mContent->HasAttr(ni->NamespaceID(), ni->NameAtom())) {
attr = RemoveAttribute(ni);
if (oldNi) {
nsRefPtr<Attr> oldAttr = GetAttribute(oldNi, true);
if (oldAttr == &aAttr) {
return oldAttr.forget();
}
} else { // SetNamedItem()
aAttr.GetName(name);
// get node-info of old attribute
ni = mContent->GetExistingAttrNameFromQName(name);
if (ni) {
attr = RemoveAttribute(ni);
}
else {
if (mContent->IsInHTMLDocument() &&
mContent->IsHTML()) {
nsContentUtils::ASCIIToLower(name);
}
rv = mContent->NodeInfo()->NodeInfoManager()->
GetNodeInfo(name, nullptr, kNameSpaceID_None,
nsIDOMNode::ATTRIBUTE_NODE, getter_AddRefs(ni));
if (NS_FAILED(rv)) {
aError.Throw(rv);
return nullptr;
}
// value is already empty
if (oldAttr) {
attr = RemoveNamedItem(oldNi, aError);
NS_ASSERTION(attr->NodeInfo()->NameAndNamespaceEquals(oldNi),
"RemoveNamedItem() called, attr->NodeInfo() should be equal to oldNi!");
}
}
nsAutoString value;
aAttr.GetValue(value);
nsRefPtr<NodeInfo> ni = aAttr.NodeInfo();
// Add the new attribute to the attribute map before updating
// its value in the element. @see bug 364413.
nsAttrKey attrkey(ni->NamespaceID(), ni->NameAtom());
@ -373,6 +378,15 @@ nsDOMAttributeMap::SetNamedItemInternal(Attr& aAttr,
return attr.forget();
}
already_AddRefed<Attr>
nsDOMAttributeMap::RemoveNamedItem(NodeInfo* aNodeInfo, ErrorResult& aError)
{
nsRefPtr<Attr> attribute = GetAttribute(aNodeInfo, true);
// This removes the attribute node from the attribute map.
aError = mContent->UnsetAttr(aNodeInfo->NamespaceID(), aNodeInfo->NameAtom(), true);
return attribute.forget();
}
NS_IMETHODIMP
nsDOMAttributeMap::RemoveNamedItem(const nsAString& aName,
nsIDOMAttr** aReturn)
@ -398,11 +412,7 @@ nsDOMAttributeMap::RemoveNamedItem(const nsAString& aName, ErrorResult& aError)
return nullptr;
}
nsRefPtr<Attr> attribute = GetAttribute(ni, true);
// This removes the attribute node from the attribute map.
aError = mContent->UnsetAttr(ni->NamespaceID(), ni->NameAtom(), true);
return attribute.forget();
return RemoveNamedItem(ni, aError);
}
@ -500,6 +510,7 @@ nsDOMAttributeMap::GetAttrNodeInfo(const nsAString& aNamespaceURI,
int32_t attrNS = name->NamespaceID();
nsIAtom* nameAtom = name->LocalName();
// we're purposefully ignoring the prefix.
if (nameSpaceID == attrNS &&
nameAtom->Equals(aLocalName)) {
nsRefPtr<mozilla::dom::NodeInfo> ni;
@ -536,11 +547,7 @@ nsDOMAttributeMap::RemoveNamedItemNS(const nsAString& aNamespaceURI,
return nullptr;
}
nsRefPtr<Attr> attr = RemoveAttribute(ni);
mozilla::dom::NodeInfo* attrNi = attr->NodeInfo();
mContent->UnsetAttr(attrNi->NamespaceID(), attrNi->NameAtom(), true);
return attr.forget();
return RemoveNamedItem(ni, aError);
}
uint32_t

View File

@ -152,6 +152,8 @@ public:
return SetNamedItemInternal(aAttr, false, aError);
}
already_AddRefed<Attr>
RemoveNamedItem(mozilla::dom::NodeInfo* aNodeInfo, ErrorResult& aError);
already_AddRefed<Attr>
RemoveNamedItem(const nsAString& aName, ErrorResult& aError);
Attr* Item(uint32_t aIndex);

View File

@ -2304,7 +2304,7 @@ nsFrameLoader::CreateStaticClone(nsIFrameLoader* aDest)
bool
nsFrameLoader::DoLoadFrameScript(const nsAString& aURL, bool aRunInGlobalScope)
{
mozilla::dom::PBrowserParent* tabParent = GetRemoteBrowser();
auto* tabParent = static_cast<TabParent*>(GetRemoteBrowser());
if (tabParent) {
return tabParent->SendLoadRemoteScript(nsString(aURL), aRunInGlobalScope);
}

View File

@ -192,7 +192,7 @@ nsHTMLContentSerializer::AppendElementStart(Element* aElement,
bool lineBreakBeforeOpen = LineBreakBeforeOpen(ns, name);
if ((mDoFormat || forceFormat) && !mPreLevel && !mDoRaw) {
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel()) {
if (mColPos && lineBreakBeforeOpen) {
AppendNewLineToString(aStr);
}
@ -225,7 +225,7 @@ nsHTMLContentSerializer::AppendElementStart(Element* aElement,
MaybeEnterInPreContent(content);
// for block elements, we increase the indentation
if ((mDoFormat || forceFormat) && !mPreLevel && !mDoRaw)
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel())
IncrIndentation(name);
// Need to keep track of OL and LI elements in order to get ordinal number
@ -280,8 +280,8 @@ nsHTMLContentSerializer::AppendElementStart(Element* aElement,
++mDisableEntityEncoding;
}
if ((mDoFormat || forceFormat) && !mPreLevel &&
!mDoRaw && LineBreakAfterOpen(ns, name)) {
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel() &&
LineBreakAfterOpen(ns, name)) {
AppendNewLineToString(aStr);
}
@ -312,18 +312,18 @@ nsHTMLContentSerializer::AppendElementEnd(Element* aElement,
bool forceFormat = !(mFlags & nsIDocumentEncoder::OutputIgnoreMozDirty) &&
content->HasAttr(kNameSpaceID_None, nsGkAtoms::mozdirty);
if ((mDoFormat || forceFormat) && !mPreLevel && !mDoRaw) {
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel()) {
DecrIndentation(name);
}
if (name == nsGkAtoms::script) {
nsCOMPtr<nsIScriptElement> script = do_QueryInterface(aElement);
if (script && script->IsMalformed()) {
if (ShouldMaintainPreLevel() && script && script->IsMalformed()) {
// We're looking at a malformed script tag. This means that the end tag
// was missing in the source. Imitate that here by not serializing the end
// tag.
--mPreLevel;
--PreLevel();
return NS_OK;
}
}
@ -351,7 +351,7 @@ nsHTMLContentSerializer::AppendElementEnd(Element* aElement,
}
}
if ((mDoFormat || forceFormat) && !mPreLevel && !mDoRaw) {
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel()) {
bool lineBreakBeforeClose = LineBreakBeforeClose(ns, name);
@ -377,8 +377,8 @@ nsHTMLContentSerializer::AppendElementEnd(Element* aElement,
MaybeLeaveFromPreContent(content);
if ((mDoFormat || forceFormat) && !mPreLevel
&& !mDoRaw && LineBreakAfterClose(ns, name)) {
if ((mDoFormat || forceFormat)&& !mDoRaw && !PreLevel()
&& LineBreakAfterClose(ns, name)) {
AppendNewLineToString(aStr);
}
else {

View File

@ -22,6 +22,7 @@
#include "mozilla/dom/Element.h"
#include "mozilla/Preferences.h"
#include "mozilla/BinarySearch.h"
#include "nsComputedDOMStyle.h"
using namespace mozilla;
using namespace mozilla::dom;
@ -356,6 +357,7 @@ nsPlainTextSerializer::AppendElementStart(Element* aElement,
if (isContainer) {
rv = DoOpenContainer(id);
mPreformatStack.push(IsElementPreformatted(mElement));
}
else {
rv = DoAddLeaf(id);
@ -389,6 +391,7 @@ nsPlainTextSerializer::AppendElementEnd(Element* aElement,
rv = NS_OK;
if (isContainer) {
rv = DoCloseContainer(id);
mPreformatStack.pop();
}
mElement = nullptr;
@ -1537,7 +1540,7 @@ nsPlainTextSerializer::Write(const nsAString& aStr)
// This mustn't be mixed with intelligent wrapping without clearing
// the mCurrentLine buffer before!!!
NS_ASSERTION(mCurrentLine.IsEmpty(),
NS_ASSERTION(mCurrentLine.IsEmpty() || IsInPre(),
"Mixed wrapping data and nonwrapping data on the same line");
if (!mCurrentLine.IsEmpty()) {
FlushLine();
@ -1755,28 +1758,22 @@ nsPlainTextSerializer::GetIdForContent(nsIContent* aContent)
return localName->IsStaticAtom() ? localName : nullptr;
}
/**
* Returns true if we currently are inside a <pre>. The check is done
* by traversing the tag stack looking for <pre> until we hit a block
* level tag which is assumed to override any <pre>:s below it in
* the stack. To do this correctly to a 100% would require access
* to style which we don't support in this converter.
*/
bool
nsPlainTextSerializer::IsInPre()
{
int32_t i = mTagStackIndex;
while(i > 0) {
if (mTagStack[i - 1] == nsGkAtoms::pre)
return true;
if (nsContentUtils::IsHTMLBlock(mTagStack[i - 1])) {
// We assume that every other block overrides a <pre>
return false;
}
--i;
}
return !mPreformatStack.empty() && mPreformatStack.top();
}
// Not a <pre> in the whole stack
bool
nsPlainTextSerializer::IsElementPreformatted(Element* aElement)
{
nsRefPtr<nsStyleContext> styleContext =
nsComputedDOMStyle::GetStyleContextForElementNoFlush(aElement, nullptr,
nullptr);
if (styleContext) {
const nsStyleText* textStyle = styleContext->StyleText();
return textStyle->WhiteSpaceOrNewlineIsSignificant();
}
return false;
}

View File

@ -22,6 +22,8 @@
#include "nsString.h"
#include "nsTArray.h"
#include <stack>
class nsIContent;
namespace mozilla {
@ -112,6 +114,9 @@ protected:
bool ShouldReplaceContainerWithPlaceholder(nsIAtom* aTag);
private:
bool IsElementPreformatted(mozilla::dom::Element* aElement);
protected:
nsString mCurrentLine;
uint32_t mHeadLevel;
@ -196,6 +201,11 @@ protected:
nsIAtom** mTagStack;
uint32_t mTagStackIndex;
// The stack indicating whether the elements we've been operating on are
// CSS preformatted elements, so that we can tell if the text inside them
// should be formatted.
std::stack<bool> mPreformatStack;
// Content in the stack above this index should be ignored:
uint32_t mIgnoreAboveIndex;

View File

@ -33,6 +33,8 @@
#include "nsIScriptElement.h"
#include "nsAttrName.h"
#include "nsParserConstants.h"
#include "nsComputedDOMStyle.h"
#include "mozilla/dom/Element.h"
static const int32_t kLongLineLen = 128;
@ -131,7 +133,7 @@ nsXHTMLContentSerializer::AppendText(nsIContent* aText,
if (NS_FAILED(rv))
return NS_ERROR_FAILURE;
if (mPreLevel > 0 || mDoRaw) {
if (mDoRaw || PreLevel() > 0) {
AppendToStringConvertLF(data, aStr);
}
else if (mDoFormat) {
@ -535,8 +537,9 @@ nsXHTMLContentSerializer::CheckElementStart(nsIContent * aContent,
int32_t namespaceID = aContent->GetNameSpaceID();
if (namespaceID == kNameSpaceID_XHTML) {
if (name == nsGkAtoms::br && mPreLevel > 0 &&
(mFlags & nsIDocumentEncoder::OutputNoFormattingInPre)) {
if (name == nsGkAtoms::br &&
(mFlags & nsIDocumentEncoder::OutputNoFormattingInPre) &&
PreLevel() > 0) {
AppendNewLineToString(aStr);
return false;
}
@ -843,41 +846,60 @@ nsXHTMLContentSerializer::LineBreakAfterClose(int32_t aNamespaceID, nsIAtom* aNa
void
nsXHTMLContentSerializer::MaybeEnterInPreContent(nsIContent* aNode)
{
if (aNode->GetNameSpaceID() != kNameSpaceID_XHTML) {
if (!ShouldMaintainPreLevel() ||
aNode->GetNameSpaceID() != kNameSpaceID_XHTML) {
return;
}
nsIAtom *name = aNode->Tag();
if (name == nsGkAtoms::pre ||
if (IsElementPreformatted(aNode) ||
name == nsGkAtoms::script ||
name == nsGkAtoms::style ||
name == nsGkAtoms::noscript ||
name == nsGkAtoms::noframes
) {
mPreLevel++;
PreLevel()++;
}
}
void
nsXHTMLContentSerializer::MaybeLeaveFromPreContent(nsIContent* aNode)
{
if (aNode->GetNameSpaceID() != kNameSpaceID_XHTML) {
if (!ShouldMaintainPreLevel() ||
aNode->GetNameSpaceID() != kNameSpaceID_XHTML) {
return;
}
nsIAtom *name = aNode->Tag();
if (name == nsGkAtoms::pre ||
if (IsElementPreformatted(aNode) ||
name == nsGkAtoms::script ||
name == nsGkAtoms::style ||
name == nsGkAtoms::noscript ||
name == nsGkAtoms::noframes
) {
--mPreLevel;
--PreLevel();
}
}
bool
nsXHTMLContentSerializer::IsElementPreformatted(nsIContent* aNode)
{
MOZ_ASSERT(ShouldMaintainPreLevel(), "We should not be calling this needlessly");
if (!aNode->IsElement()) {
return false;
}
nsRefPtr<nsStyleContext> styleContext =
nsComputedDOMStyle::GetStyleContextForElementNoFlush(aNode->AsElement(),
nullptr, nullptr);
if (styleContext) {
const nsStyleText* textStyle = styleContext->StyleText();
return textStyle->WhiteSpaceOrNewlineIsSignificant();
}
return false;
}
void
nsXHTMLContentSerializer::SerializeLIValueAttribute(nsIContent* aElement,
nsAString& aStr)

View File

@ -93,6 +93,10 @@ class nsXHTMLContentSerializer : public nsXMLContentSerializer {
const nsAString& aURI,
nsAString& aEscapedURI);
private:
bool IsElementPreformatted(nsIContent* aNode);
protected:
nsCOMPtr<nsIEntityConverter> mEntityConverter;
/*

View File

@ -187,7 +187,7 @@ nsXMLContentSerializer::AppendText(nsIContent* aText,
if (NS_FAILED(rv))
return NS_ERROR_FAILURE;
if (mPreLevel > 0 || mDoRaw) {
if (mDoRaw || PreLevel() > 0) {
AppendToStringConvertLF(data, aStr);
}
else if (mDoFormat) {
@ -214,7 +214,7 @@ nsXMLContentSerializer::AppendCDATASection(nsIContent* aCDATASection,
NS_NAMED_LITERAL_STRING(cdata , "<![CDATA[");
if (mPreLevel > 0 || mDoRaw) {
if (mDoRaw || PreLevel() > 0) {
AppendToString(cdata, aStr);
}
else if (mDoFormat) {
@ -260,7 +260,7 @@ nsXMLContentSerializer::AppendProcessingInstruction(nsIContent* aPI,
start.AppendLiteral("<?");
start.Append(target);
if (mPreLevel > 0 || mDoRaw) {
if (mDoRaw || PreLevel() > 0) {
AppendToString(start, aStr);
}
else if (mDoFormat) {
@ -318,7 +318,7 @@ nsXMLContentSerializer::AppendComment(nsIContent* aComment,
NS_NAMED_LITERAL_STRING(startComment, "<!--");
if (mPreLevel > 0 || mDoRaw) {
if (mDoRaw || PreLevel() > 0) {
AppendToString(startComment, aStr);
}
else if (mDoFormat) {
@ -693,7 +693,7 @@ nsXMLContentSerializer::SerializeAttr(const nsAString& aPrefix,
attrString.Append(sValue);
attrString.Append(cDelimiter);
}
if (mPreLevel > 0 || mDoRaw) {
if (mDoRaw || PreLevel() > 0) {
AppendToStringConvertLF(attrString, aStr);
}
else if (mDoFormat) {
@ -898,7 +898,7 @@ nsXMLContentSerializer::AppendElementStart(Element* aElement,
nsIAtom *name = content->Tag();
bool lineBreakBeforeOpen = LineBreakBeforeOpen(content->GetNameSpaceID(), name);
if ((mDoFormat || forceFormat) && !mPreLevel && !mDoRaw) {
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel()) {
if (mColPos && lineBreakBeforeOpen) {
AppendNewLineToString(aStr);
}
@ -939,7 +939,7 @@ nsXMLContentSerializer::AppendElementStart(Element* aElement,
MaybeEnterInPreContent(content);
if ((mDoFormat || forceFormat) && !mPreLevel && !mDoRaw) {
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel()) {
IncrIndentation(name);
}
@ -949,8 +949,8 @@ nsXMLContentSerializer::AppendElementStart(Element* aElement,
AppendEndOfElementStart(aOriginalElement, name, content->GetNameSpaceID(),
aStr);
if ((mDoFormat || forceFormat) && !mPreLevel
&& !mDoRaw && LineBreakAfterOpen(content->GetNameSpaceID(), name)) {
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel()
&& LineBreakAfterOpen(content->GetNameSpaceID(), name)) {
AppendNewLineToString(aStr);
}
@ -987,7 +987,7 @@ nsXMLContentSerializer::AppendElementEnd(Element* aElement,
nsIAtom *name = content->Tag();
if ((mDoFormat || forceFormat) && !mPreLevel && !mDoRaw) {
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel()) {
DecrIndentation(name);
}
@ -1009,7 +1009,7 @@ nsXMLContentSerializer::AppendElementEnd(Element* aElement,
ConfirmPrefix(tagPrefix, tagNamespaceURI, aElement, false);
NS_ASSERTION(!debugNeedToPushNamespace, "Can't push namespaces in closing tag!");
if ((mDoFormat || forceFormat) && !mPreLevel && !mDoRaw) {
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel()) {
bool lineBreakBeforeClose = LineBreakBeforeClose(content->GetNameSpaceID(), name);
@ -1041,8 +1041,8 @@ nsXMLContentSerializer::AppendElementEnd(Element* aElement,
MaybeLeaveFromPreContent(content);
if ((mDoFormat || forceFormat) && !mPreLevel
&& !mDoRaw && LineBreakAfterClose(content->GetNameSpaceID(), name)) {
if ((mDoFormat || forceFormat) && !mDoRaw && !PreLevel()
&& LineBreakAfterClose(content->GetNameSpaceID(), name)) {
AppendNewLineToString(aStr);
}
else {
@ -1217,11 +1217,12 @@ void
nsXMLContentSerializer::MaybeEnterInPreContent(nsIContent* aNode)
{
// support of the xml:space attribute
if (aNode->HasAttr(kNameSpaceID_XML, nsGkAtoms::space)) {
if (ShouldMaintainPreLevel() &&
aNode->HasAttr(kNameSpaceID_XML, nsGkAtoms::space)) {
nsAutoString space;
aNode->GetAttr(kNameSpaceID_XML, nsGkAtoms::space, space);
if (space.EqualsLiteral("preserve"))
++mPreLevel;
++PreLevel();
}
}
@ -1229,11 +1230,12 @@ void
nsXMLContentSerializer::MaybeLeaveFromPreContent(nsIContent* aNode)
{
// support of the xml:space attribute
if (aNode->HasAttr(kNameSpaceID_XML, nsGkAtoms::space)) {
if (ShouldMaintainPreLevel() &&
aNode->HasAttr(kNameSpaceID_XML, nsGkAtoms::space)) {
nsAutoString space;
aNode->GetAttr(kNameSpaceID_XML, nsGkAtoms::space, space);
if (space.EqualsLiteral("preserve"))
--mPreLevel;
--PreLevel();
}
}
@ -1442,7 +1444,7 @@ nsXMLContentSerializer::AppendWrapped_NonWhitespaceSequence(
colPos = mColPos;
}
else {
if (mDoFormat && !mPreLevel && !onceAgainBecauseWeAddedBreakInFront) {
if (mDoFormat && !mDoRaw && !PreLevel() && !onceAgainBecauseWeAddedBreakInFront) {
colPos = mIndent.Length();
}
else
@ -1711,3 +1713,10 @@ nsXMLContentSerializer::AppendToStringWrapped(const nsASingleFragmentString& aSt
}
}
}
bool
nsXMLContentSerializer::ShouldMaintainPreLevel() const
{
// Only attempt to maintain the pre level for consumers who care about it.
return !mDoRaw || (mFlags & nsIDocumentEncoder::OutputNoFormattingInPre);
}

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