зеркало из https://github.com/mozilla/gecko-dev.git
Merge m-c to b2g-inbound a=merge
This commit is contained in:
Коммит
adad8db983
2
CLOBBER
2
CLOBBER
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -490,7 +490,7 @@ appUpdater.prototype =
|
|||
return;
|
||||
}
|
||||
|
||||
this.selectPanel("apply");
|
||||
this.selectPanel("applyBillboard");
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>'
|
||||
}
|
|
@ -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>");
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}));
|
||||
|
|
@ -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();
|
||||
|
|
|
@ -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."
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
}));
|
||||
|
|
|
@ -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")
|
||||
}));
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -20,7 +20,9 @@ describe("loop.roomViews", function () {
|
|||
mozLoop: {
|
||||
getAudioBlob: sinon.stub()
|
||||
}
|
||||
}
|
||||
},
|
||||
addEventListener: function() {},
|
||||
removeEventListener: function() {}
|
||||
};
|
||||
loop.shared.mixins.setRootObject(fakeWindow);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"})
|
||||
)
|
||||
),
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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");
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"],
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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).");
|
||||
|
|
|
@ -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).");
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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]
|
||||
|
|
|
@ -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", {});
|
||||
|
|
@ -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: []
|
||||
}];
|
|
@ -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: []
|
||||
}];
|
|
@ -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: []
|
||||
}];
|
|
@ -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) {
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -93,6 +93,10 @@ class nsXHTMLContentSerializer : public nsXMLContentSerializer {
|
|||
const nsAString& aURI,
|
||||
nsAString& aEscapedURI);
|
||||
|
||||
private:
|
||||
bool IsElementPreformatted(nsIContent* aNode);
|
||||
|
||||
protected:
|
||||
nsCOMPtr<nsIEntityConverter> mEntityConverter;
|
||||
|
||||
/*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче