Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2016-01-15 11:48:08 +01:00
commit b5e5b490e9
465 changed files with 6111 additions and 20245 deletions

View File

@ -5,7 +5,6 @@
obj*/**
# Temporarily ignore HTML files that still need to be fixed.
browser/extensions/loop/**/*.html
devtools/**/*.html
# We ignore all these directories by default, until we get them enabled.
@ -89,40 +88,8 @@ browser/extensions/shumway/**
browser/fuel/**
browser/locales/**
# Loop specific exclusions
# This file currently uses a non-standard (and not on a standards track)
# if statement within catch.
browser/extensions/loop/content/modules/MozLoopWorker.js
# This file currently uses es7 features eslint issue:
# https://github.com/eslint/espree/issues/125
browser/extensions/loop/content/modules/MozLoopAPI.jsm
# Need to fix the configuration for this.
browser/extensions/loop/bootstrap.js
# Need to drop the preprocessing (bug 1212428)
browser/extensions/loop/content/preferences/prefs.js
# Libs we don't need to check
browser/extensions/loop/content/panels/vendor
browser/extensions/loop/content/shared/vendor
browser/extensions/loop/standalone/content/vendor
# Libs we don't need to check
browser/extensions/loop/test/shared/vendor
# Coverage files
browser/extensions/loop/test/coverage
# These are generated react files that we don't need to check
browser/extensions/loop/content/panels/js/conversation.js
browser/extensions/loop/content/panels/js/conversationViews.js
browser/extensions/loop/content/panels/js/panel.js
browser/extensions/loop/content/panels/js/roomViews.js
browser/extensions/loop/content/panels/js/feedbackViews.js
browser/extensions/loop/content/shared/js/textChatView.js
browser/extensions/loop/content/shared/js/linkifiedTextView.js
browser/extensions/loop/content/shared/js/views.js
browser/extensions/loop/standalone/content/js/standaloneRoomViews.js
browser/extensions/loop/standalone/content/js/webapp.js
browser/extensions/loop/ui/ui-showcase.js
# Don't need to check the built tree
browser/extensions/loop/standalone/dist
# Ignore all of loop since it is imported from github and checked at source.
browser/extensions/loop/**
# devtools/ exclusions
# Ignore d3

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>
@ -160,7 +160,7 @@
<project name="platform/hardware/qcom/wlan" path="hardware/qcom/wlan" revision="82d8c482d59926ff27c53f1fcc8a0ce3554e5491"/>
<project name="platform/hardware/ril" path="hardware/ril" revision="4782b037af55a8d61078305df6a4c31c1424623c"/>
<project name="platform_external_libnfc-nci" path="external/libnfc-nci" remote="b2g" revision="cac2be832f7bf748d8f0de7eac93009ef7c2eea9"/>
<project name="hardware_qcom_display" path="hardware/qcom/display" remote="b2g" revision="288427e047fcdadba1b907fc886bf1142e1f646e"/>
<project name="hardware_qcom_display" path="hardware/qcom/display" remote="b2g" revision="9312b15e14ee19686ec7eac7b4cf2035199e92a6"/>
<project name="platform_hardware_broadcom_libbt" path="hardware/broadcom/libbt" remote="b2g" revision="c927f10590b39bfe35976e01aed0969afeed3ba8"/>
<project name="platform_hardware_libhardware" path="hardware/libhardware" remote="b2g" revision="c4bdd7888f2ab3069f6f8853915a806ebd390320"/>
<project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="5f4b68c799927b6e078f987b12722c3a6ccd4a45"/>

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>

View File

@ -1,9 +1,9 @@
{
"git": {
"git_revision": "2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f",
"git_revision": "44cc5dfeda9251188c6547f2d5903ee980a5361e",
"remote": "https://git.mozilla.org/releases/gaia.git",
"branch": ""
},
"revision": "ed05ae5ca5b9a6d8f01386c1f9345c041971afd8",
"revision": "765ddb65fc152b691b464fb83168345f2292fe2e",
"repo_path": "integration/gaia-central"
}

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>

View File

@ -21,7 +21,7 @@
<!--
B2G repositories for all targets
-->
<project name="gaia" path="gaia" remote="mozillaorg" revision="2d6c11a1e82bd9f9b7491863c8d1ea4a14aea69f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="44cc5dfeda9251188c6547f2d5903ee980a5361e"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="4a962bdab532e18f53e9d2d114c349983262c6b7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="99c333dab00ed79baff9e1cf76b320aee8e1c123"/>
<project name="platform_hardware_libhardware_moz" path="hardware/libhardware_moz" remote="b2g" revision="fdf3a143dc777e5f9d33a88373af7ea161d3b440"/>
@ -159,7 +159,7 @@
<project name="platform/hardware/broadcom/libbt" path="hardware/broadcom/libbt" revision="399fe3d3c8f38c599a56becddc456133e62a5d70"/>
<project name="platform/hardware/broadcom/wlan" path="hardware/broadcom/wlan" revision="3f3134d5cb19d5ace48d36d0100467a545d430eb"/>
<project name="platform/hardware/qcom/audio" path="hardware/qcom/audio" revision="810c3dd29d009822a71eba9910e429a9ad114533"/>
<project name="hardware_qcom_display" path="hardware/qcom/display" remote="b2g" revision="d8a56d7215bd26a61e43dcde20e64826a5fec265"/>
<project name="hardware_qcom_display" path="hardware/qcom/display" remote="b2g" revision="7b5967bbd90cb193b489d8f8668bc945384bd9b1"/>
<project name="platform/hardware/qcom/keymaster" path="hardware/qcom/keymaster" revision="eaede9f8bc206736a889bc57817047c31e205589"/>
<project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="12364db20d6710f0003a4f00962c9790ad3c13e3"/>
<project name="platform/hardware/qcom/msm8x74" path="hardware/qcom/msm8x74" revision="f09920b2b488cf68bcbe9f7bbbde84a509a1d7a5"/>

View File

@ -56,9 +56,21 @@ window.addEventListener("pagehide", function() {
});
window.addEventListener("keypress", ev => {
// focus the search-box on keypress
if (document.activeElement.id == "searchText") // unless already focussed
if (ev.defaultPrevented) {
return;
}
// don't focus the search-box on keypress if something other than the
// body or document element has focus - don't want to steal input from other elements
// Make an exception for <a> and <button> elements (and input[type=button|submit])
// which don't usefully take keypresses anyway.
// (except space, which is handled below)
if (document.activeElement && document.activeElement != document.body &&
document.activeElement != document.documentElement &&
!["a", "button"].includes(document.activeElement.localName) &&
!document.activeElement.matches("input:-moz-any([type=button],[type=submit])")) {
return;
}
let modifiers = ev.ctrlKey + ev.altKey + ev.metaKey;
// ignore Ctrl/Cmd/Alt, but not Shift

View File

@ -108,11 +108,11 @@
<field name="mTabsProgressListeners">
[]
</field>
<field name="mTabListeners">
[]
<field name="_tabListeners">
new Map()
</field>
<field name="mTabFilters">
[]
<field name="_tabFilters">
new Map()
</field>
<field name="mIsBusy">
false
@ -1097,7 +1097,7 @@
true, false);
}
var listener = this.mTabListeners[this.tabContainer.selectedIndex] || null;
var listener = this._tabListeners.get(this.mCurrentTab);
if (listener && listener.mStateFlags) {
this._callProgressListeners(null, "onUpdateCurrentBrowser",
[listener.mStateFlags, listener.mStatus,
@ -1506,8 +1506,7 @@
// Unhook our progress listener.
let tab = this.getTabForBrowser(aBrowser);
let index = tab._tPos;
let filter = this.mTabFilters[index];
let filter = this._tabFilters.get(tab);
aBrowser.webProgress.removeProgressListener(filter);
// Make sure the browser is destroyed so it unregisters from observer notifications
aBrowser.destroy();
@ -1881,8 +1880,8 @@
.createInstance(Components.interfaces.nsIWebProgress);
filter.addProgressListener(tabListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
b.webProgress.addProgressListener(filter, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
this.mTabListeners[position] = tabListener;
this.mTabFilters[position] = filter;
this._tabListeners.set(t, tabListener);
this._tabFilters.set(t, filter);
b.droppedLinkHandler = handleDroppedLink;
@ -2254,12 +2253,13 @@
}
// Remove the tab's filter and progress listener.
const filter = this.mTabFilters[aTab._tPos];
const filter = this._tabFilters.get(aTab);
browser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(this.mTabListeners[aTab._tPos]);
this.mTabListeners[aTab._tPos].destroy();
const listener = this._tabListeners.get(aTab);
filter.removeProgressListener(listener);
listener.destroy();
if (browser.registeredOpenURI && !aTabWillBeMoved) {
this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI);
@ -2320,11 +2320,8 @@
}
// We're going to remove the tab and the browser now.
// Clean up mTabFilters and mTabListeners now rather than in
// _beginRemoveTab, so that their size is always in sync with the
// number of tabs and browsers (the xbl destructor depends on this).
this.mTabFilters.splice(aTab._tPos, 1);
this.mTabListeners.splice(aTab._tPos, 1);
this._tabFilters.delete(aTab);
this._tabListeners.delete(aTab);
var browser = this.getBrowserForTab(aTab);
this._outerWindowIDBrowserMap.delete(browser.outerWindowID);
@ -2576,9 +2573,8 @@
<body>
<![CDATA[
// Unhook our progress listener
let index = aOurTab._tPos;
const filter = this.mTabFilters[index];
let tabListener = this.mTabListeners[index];
const filter = this._tabFilters.get(aOurTab);
let tabListener = this._tabListeners.get(aOurTab);
let ourBrowser = this.getBrowserForTab(aOurTab);
ourBrowser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(tabListener);
@ -2618,8 +2614,8 @@
aOtherBrowser.permanentKey = ourPermanentKey;
// Restore the progress listener
this.mTabListeners[index] = tabListener =
this.mTabProgressListener(aOurTab, ourBrowser, false, false);
tabListener = this.mTabProgressListener(aOurTab, ourBrowser, false, false);
this._tabListeners.set(aOurTab, tabListener);
const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
filter.addProgressListener(tabListener, notifyAll);
@ -2885,9 +2881,6 @@
this._lastRelatedTab = null;
this.mTabFilters.splice(aIndex, 0, this.mTabFilters.splice(aTab._tPos, 1)[0]);
this.mTabListeners.splice(aIndex, 0, this.mTabListeners.splice(aTab._tPos, 1)[0]);
let wasFocused = (document.activeElement == this.mCurrentTab);
aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1;
@ -4198,8 +4191,8 @@
const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"]
.createInstance(nsIWebProgress);
filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL);
this.mTabListeners[0] = tabListener;
this.mTabFilters[0] = filter;
this._tabListeners.set(this.mCurrentTab, tabListener);
this._tabFilters.set(this.mCurrentTab, filter);
this.webProgress.addProgressListener(filter, nsIWebProgress.NOTIFY_ALL);
this.style.backgroundColor =
@ -4252,18 +4245,22 @@
Services.obs.removeObserver(this, "live-resize-start", false);
Services.obs.removeObserver(this, "live-resize-end", false);
for (var i = 0; i < this.mTabListeners.length; ++i) {
let browser = this.getBrowserAtIndex(i);
for (let tab of this.tabs) {
let browser = tab.linkedBrowser;
if (browser.registeredOpenURI) {
this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI);
this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI);
delete browser.registeredOpenURI;
}
browser.webProgress.removeProgressListener(this.mTabFilters[i]);
this.mTabFilters[i].removeProgressListener(this.mTabListeners[i]);
this.mTabFilters[i] = null;
this.mTabListeners[i].destroy();
this.mTabListeners[i] = null;
let filter = this._tabFilters.get(tab);
let listener = this._tabListeners.get(tab);
browser.webProgress.removeProgressListener(filter);
filter.removeProgressListener(listener);
listener.destroy();
this._tabFilters.delete(tab);
this._tabListeners.delete(tab);
}
const nsIEventListenerService =
Components.interfaces.nsIEventListenerService;

View File

@ -290,7 +290,7 @@ skip-if = os == "mac" || e10s # bug 967013; e10s: bug 1094761 - test hits the ne
skip-if = !datareporting
[browser_devedition.js]
[browser_devices_get_user_media.js]
skip-if = buildapp == 'mulet' || (os == "linux" && debug) || e10s # linux: bug 976544; e10s: bug 1071623
skip-if = buildapp == 'mulet' || (os == "linux" && debug) # linux: bug 976544
[browser_devices_get_user_media_about_urls.js]
skip-if = e10s # Bug 1071623
[browser_devices_get_user_media_in_frame.js]

View File

@ -4,6 +4,15 @@
requestLongerTimeout(2);
const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
let frameScript = function() {
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
"@mozilla.org/mediaManagerService;1",
"nsIMediaManagerService");
const kObservedTopics = [
"getUserMedia:response:allow",
"getUserMedia:revoke",
@ -13,13 +22,6 @@ const kObservedTopics = [
"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))
@ -28,41 +30,120 @@ function observer(aSubject, aTopic, aData) {
++gObservedTopics[aTopic];
}
function promiseObserverCalled(aTopic, aAction) {
let deferred = Promise.defer();
kObservedTopics.forEach(topic => {
Services.obs.addObserver(observer, topic, false);
});
addMessageListener("Test:ExpectObserverCalled", ({data}) => {
sendAsyncMessage("Test:ExpectObserverCalled:Reply",
{count: gObservedTopics[data]});
if (data in gObservedTopics)
--gObservedTopics[data];
});
addMessageListener("Test:TodoObserverNotCalled", ({data}) => {
sendAsyncMessage("Test:TodoObserverNotCalled:Reply",
{count: gObservedTopics[data]});
if (gObservedTopics[data] == 1)
gObservedTopics[data] = 0;
});
addMessageListener("Test:ExpectNoObserverCalled", data => {
sendAsyncMessage("Test:ExpectNoObserverCalled:Reply", gObservedTopics);
gObservedTopics = {};
});
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";
}
addMessageListener("Test:GetMediaCaptureState", data => {
sendAsyncMessage("Test:MediaCaptureState", _getMediaCaptureState());
});
addMessageListener("Test:WaitForObserverCall", ({data}) => {
let topic = data;
Services.obs.addObserver(function observer() {
ok(true, "got " + aTopic + " notification");
Services.obs.removeObserver(observer, aTopic);
sendAsyncMessage("Test:ObserverCalled", topic);
Services.obs.removeObserver(observer, topic);
if (kObservedTopics.indexOf(aTopic) != -1) {
if (!(aTopic in gObservedTopics))
gObservedTopics[aTopic] = -1;
if (kObservedTopics.indexOf(topic) != -1) {
if (!(topic in gObservedTopics))
gObservedTopics[topic] = -1;
else
--gObservedTopics[aTopic];
--gObservedTopics[topic];
}
}, topic, false);
});
deferred.resolve();
}, aTopic, false);
}; // end of framescript
if (aAction)
aAction();
function _mm() {
return gBrowser.selectedBrowser.messageManager;
}
return deferred.promise;
function promiseObserverCalled(aTopic) {
return new Promise(resolve => {
let mm = _mm();
mm.addMessageListener("Test:ObserverCalled", function listener({data}) {
if (data == aTopic) {
ok(true, "got " + aTopic + " notification");
mm.removeMessageListener("Test:ObserverCalled", listener);
resolve();
}
});
mm.sendAsyncMessage("Test:WaitForObserverCall", aTopic);
});
}
function expectObserverCalled(aTopic) {
is(gObservedTopics[aTopic], 1, "expected notification " + aTopic);
if (aTopic in gObservedTopics)
--gObservedTopics[aTopic];
return new Promise(resolve => {
let mm = _mm();
mm.addMessageListener("Test:ExpectObserverCalled:Reply",
function listener({data}) {
is(data.count, 1, "expected notification " + aTopic);
mm.removeMessageListener("Test:ExpectObserverCalled:Reply", listener);
resolve();
});
mm.sendAsyncMessage("Test:ExpectObserverCalled", aTopic);
});
}
function expectNoObserverCalled() {
for (let topic in gObservedTopics) {
if (gObservedTopics[topic])
is(gObservedTopics[topic], 0, topic + " notification unexpected");
}
gObservedTopics = {};
return new Promise(resolve => {
let mm = _mm();
mm.addMessageListener("Test:ExpectNoObserverCalled:Reply",
function listener({data}) {
mm.removeMessageListener("Test:ExpectNoObserverCalled:Reply", listener);
for (let topic in data) {
if (data[topic])
is(data[topic], 0, topic + " notification unexpected");
}
resolve();
});
mm.sendAsyncMessage("Test:ExpectNoObserverCalled");
});
}
function promiseTodoObserverNotCalled(aTopic) {
return new Promise(resolve => {
let mm = _mm();
mm.addMessageListener("Test:TodoObserverNotCalled:Reply",
function listener({data}) {
mm.removeMessageListener("Test:TodoObserverNotCalled:Reply", listener);
resolve(data.count);
});
mm.sendAsyncMessage("Test:TodoObserverNotCalled", aTopic);
});
}
function promiseMessage(aMessage, aAction) {
@ -161,37 +242,46 @@ function activateSecondaryAction(aAction) {
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";
return new Promise(resolve => {
let mm = _mm();
mm.addMessageListener("Test:MediaCaptureState", ({data}) => {
resolve(data);
});
mm.sendAsyncMessage("Test:GetMediaCaptureState");
});
}
function promiseRequestDevice(aRequestAudio, aRequestVideo) {
info("requesting devices");
return ContentTask.spawn(gBrowser.selectedBrowser,
{aRequestAudio, aRequestVideo},
function*(args) {
content.wrappedJSObject.requestDevice(args.aRequestAudio,
args.aRequestVideo);
});
}
function* closeStream(aAlreadyClosed) {
expectNoObserverCalled();
yield expectNoObserverCalled();
let promise;
if (!aAlreadyClosed)
promise = promiseObserverCalled("recording-device-events");
info("closing the stream");
content.wrappedJSObject.closeStream();
yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
content.wrappedJSObject.closeStream();
});
if (!aAlreadyClosed)
yield promiseObserverCalled("recording-device-events");
yield promise;
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (!aAlreadyClosed)
expectObserverCalled("recording-window-ended");
yield expectObserverCalled("recording-window-ended");
yield* assertWebRTCIndicatorStatus(null);
}
@ -217,7 +307,7 @@ function* checkSharingUI(aExpected) {
}
function* checkNotSharing() {
is(getMediaCaptureState(), "none", "expected nothing to be shared");
is((yield getMediaCaptureState()), "none", "expected nothing to be shared");
ok(!PopupNotifications.getNotification("webRTC-sharingDevices"),
"no webRTC-sharingDevices popup notification");
@ -232,11 +322,10 @@ var gTests = [
{
desc: "getUserMedia audio+video",
run: function checkAudioVideo() {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
"webRTC-shareDevices-notification-icon", "anchored to device icon");
@ -248,9 +337,9 @@ var gTests = [
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
is((yield getMediaCaptureState()), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
@ -262,11 +351,10 @@ var gTests = [
{
desc: "getUserMedia audio only",
run: function checkAudioOnly() {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
"webRTC-shareMicrophone-notification-icon", "anchored to mic icon");
@ -274,13 +362,16 @@ var gTests = [
is(PopupNotifications.panel.firstChild.getAttribute("popupid"),
"webRTC-shareMicrophone", "panel using microphone icon");
enableDevice("Microphone", true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "Microphone", "expected microphone to be shared");
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
is((yield getMediaCaptureState()), "Microphone",
"expected microphone to be shared");
yield indicator;
yield checkSharingUI({audio: true});
@ -291,11 +382,10 @@ var gTests = [
{
desc: "getUserMedia video only",
run: function checkVideoOnly() {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(false, true);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(false, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID,
"webRTC-shareDevices-notification-icon", "anchored to device icon");
@ -307,9 +397,9 @@ var gTests = [
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "Camera", "expected camera to be shared");
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
is((yield getMediaCaptureState()), "Camera", "expected camera to be shared");
yield indicator;
yield checkSharingUI({video: true});
@ -320,14 +410,14 @@ var gTests = [
{
desc: "getUserMedia audio+video, user disables video",
run: function checkDisableVideo() {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
// disable the camera
enableDevice("Microphone", true);
enableDevice("Camera", false);
let indicator = promiseIndicatorWindow();
@ -338,9 +428,9 @@ var gTests = [
// reset the menuitem to have no impact on the following tests.
enableDevice("Camera", true);
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "Microphone",
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
is((yield getMediaCaptureState()), "Microphone",
"expected microphone to be shared");
yield indicator;
@ -352,15 +442,15 @@ var gTests = [
{
desc: "getUserMedia audio+video, user disables audio",
run: function checkDisableAudio() {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
// disable the microphone
enableDevice("Microphone", false);
enableDevice("Camera", true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
@ -370,9 +460,9 @@ var gTests = [
// reset the menuitem to have no impact on the following tests.
enableDevice("Microphone", true);
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "Camera",
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
is((yield getMediaCaptureState()), "Camera",
"expected microphone to be shared");
yield indicator;
@ -384,11 +474,10 @@ var gTests = [
{
desc: "getUserMedia audio+video, user disables both audio and video",
run: function checkDisableAudioVideo() {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
// disable the camera and microphone
@ -403,8 +492,8 @@ var gTests = [
enableDevice("Camera", true);
enableDevice("Microphone", true);
expectObserverCalled("getUserMedia:response:deny");
expectObserverCalled("recording-window-ended");
yield expectObserverCalled("getUserMedia:response:deny");
yield expectObserverCalled("recording-window-ended");
yield checkNotSharing();
}
},
@ -412,19 +501,18 @@ var gTests = [
{
desc: "getUserMedia audio+video, user clicks \"Don't Share\"",
run: function checkDontShare() {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
yield promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
expectObserverCalled("getUserMedia:response:deny");
expectObserverCalled("recording-window-ended");
yield expectObserverCalled("getUserMedia:response:deny");
yield expectObserverCalled("recording-window-ended");
yield checkNotSharing();
}
},
@ -432,20 +520,19 @@ var gTests = [
{
desc: "getUserMedia audio+video: stop sharing",
run: function checkStopSharing() {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
is((yield getMediaCaptureState()), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
@ -455,17 +542,16 @@ var gTests = [
activateSecondaryAction(kActionDeny);
yield promiseObserverCalled("recording-device-events");
expectObserverCalled("getUserMedia:revoke");
yield expectObserverCalled("getUserMedia:revoke");
yield promiseNoPopupNotification("webRTC-sharingDevices");
expectObserverCalled("recording-window-ended");
yield expectObserverCalled("recording-window-ended");
if (gObservedTopics["recording-device-events"] == 1) {
if ((yield promiseTodoObserverNotCalled("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 expectNoObserverCalled();
yield checkNotSharing();
// the stream is already closed, but this will do some cleanup anyway
@ -476,20 +562,19 @@ var 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");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "CameraAndMicrophone",
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
is((yield getMediaCaptureState()), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
@ -498,16 +583,17 @@ var gTests = [
yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices"));
info("reloading the web page");
yield promiseObserverCalled("recording-device-events",
() => { content.location.reload(); });
promise = promiseObserverCalled("recording-device-events");
content.location.reload();
yield promise;
yield promiseNoPopupNotification("webRTC-sharingDevices");
if (gObservedTopics["recording-device-events"] == 1) {
if ((yield promiseTodoObserverNotCalled("recording-device-events")) == 1) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
gObservedTopics["recording-device-events"] = 0;
}
expectObserverCalled("recording-window-ended");
expectNoObserverCalled();
yield expectObserverCalled("recording-window-ended");
yield expectNoObserverCalled();
yield checkNotSharing();
}
},
@ -519,10 +605,10 @@ var gTests = [
function checkPerm(aRequestAudio, aRequestVideo, aAllowAudio, aAllowVideo,
aExpectedAudioPerm, aExpectedVideoPerm, aNever) {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(aRequestAudio, aRequestVideo);
yield promise;
yield expectObserverCalled("getUserMedia:request");
let noAudio = aAllowAudio === undefined;
is(elt("webRTC-selectMicrophone").hidden, noAudio,
@ -543,8 +629,8 @@ var gTests = [
});
let expected = [];
if (expectedMessage == "ok") {
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
if (aAllowVideo)
expected.push("Camera");
if (aAllowAudio)
@ -552,16 +638,16 @@ var gTests = [
expected = expected.join("And");
}
else {
expectObserverCalled("getUserMedia:response:deny");
expectObserverCalled("recording-window-ended");
yield expectObserverCalled("getUserMedia:response:deny");
yield expectObserverCalled("recording-window-ended");
expected = "none";
}
is(getMediaCaptureState(), expected,
is((yield getMediaCaptureState()), expected,
"expected " + expected + " to be shared");
function checkDevicePermissions(aDevice, aExpected) {
let Perms = Services.perms;
let uri = content.document.documentURIObject;
let uri = gBrowser.selectedBrowser.documentURI;
let devicePerms = Perms.testExactPermission(uri, aDevice);
if (aExpected === undefined)
is(devicePerms, Perms.UNKNOWN_ACTION, "no " + aDevice + " persistent permissions");
@ -622,7 +708,8 @@ var gTests = [
function usePerm(aAllowAudio, aAllowVideo, aRequestAudio, aRequestVideo,
aExpectStream) {
let Perms = Services.perms;
let uri = content.document.documentURIObject;
let uri = gBrowser.selectedBrowser.documentURI;
if (aAllowAudio !== undefined) {
Perms.add(uri, "microphone", aAllowAudio ? Perms.ALLOW_ACTION
: Perms.DENY_ACTION);
@ -632,31 +719,31 @@ var gTests = [
: Perms.DENY_ACTION);
}
let gum = function() {
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
};
if (aExpectStream === undefined) {
// Check that we get a prompt.
yield promisePopupNotificationShown("webRTC-shareDevices", gum);
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(aRequestAudio, aRequestVideo);
yield promise;
yield expectObserverCalled("getUserMedia:request");
// Deny the request to cleanup...
yield promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
expectObserverCalled("getUserMedia:response:deny");
expectObserverCalled("recording-window-ended");
yield expectObserverCalled("getUserMedia:response:deny");
yield expectObserverCalled("recording-window-ended");
}
else {
let expectedMessage = aExpectStream ? "ok" : permissionError;
yield promiseMessage(expectedMessage, gum);
let promise = promiseMessage(expectedMessage);
yield promiseRequestDevice(aRequestAudio, aRequestVideo);
yield promise;
if (expectedMessage == "ok") {
expectObserverCalled("getUserMedia:request");
yield expectObserverCalled("getUserMedia:request");
yield promiseNoPopupNotification("webRTC-shareDevices");
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
// Check what's actually shared.
let expected = [];
@ -665,13 +752,13 @@ var gTests = [
if (aAllowAudio && aRequestAudio)
expected.push("Microphone");
expected = expected.join("And");
is(getMediaCaptureState(), expected,
is((yield getMediaCaptureState()), expected,
"expected " + expected + " to be shared");
yield closeStream();
}
else {
expectObserverCalled("recording-window-ended");
yield expectObserverCalled("recording-window-ended");
}
}
@ -740,7 +827,7 @@ var gTests = [
run: function checkStopSharingRemovesPersistentPermissions() {
function stopAndCheckPerm(aRequestAudio, aRequestVideo) {
let Perms = Services.perms;
let uri = content.document.documentURIObject;
let uri = gBrowser.selectedBrowser.documentURI;
// Initially set both permissions to 'allow'.
Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
@ -748,12 +835,13 @@ var gTests = [
let indicator = promiseIndicatorWindow();
// Start sharing what's been requested.
yield promiseMessage("ok", () => {
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
});
expectObserverCalled("getUserMedia:request");
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
let promise = promiseMessage("ok");
yield promiseRequestDevice(aRequestAudio, aRequestVideo);
yield promise;
yield expectObserverCalled("getUserMedia:request");
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
yield indicator;
yield checkSharingUI({video: aRequestVideo, audio: aRequestAudio});
@ -770,14 +858,13 @@ var gTests = [
activateSecondaryAction(kActionDeny);
yield promiseObserverCalled("recording-device-events");
expectObserverCalled("getUserMedia:revoke");
yield expectObserverCalled("getUserMedia:revoke");
yield promiseNoPopupNotification("webRTC-sharingDevices");
expectObserverCalled("recording-window-ended");
yield expectObserverCalled("recording-window-ended");
if (gObservedTopics["recording-device-events"] == 1) {
if ((yield promiseTodoObserverNotCalled("recording-device-events")) == 1) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
gObservedTopics["recording-device-events"] = 0;
}
// Check that permissions have been removed as expected.
@ -812,20 +899,19 @@ var gTests = [
{
desc: "test showSharingDoorhanger",
run: function checkShowSharingDoorhanger() {
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
info("requesting devices");
content.wrappedJSObject.requestDevice(false, true);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(false, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(false, true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "Camera", "expected camera to be shared");
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
is((yield getMediaCaptureState()), "Camera", "expected camera to be shared");
yield indicator;
yield checkSharingUI({video: true});
@ -845,7 +931,7 @@ var gTests = [
PopupNotifications.panel.firstChild.button.click();
ok(!PopupNotifications.isPanelOpen, "notification panel closed");
expectNoObserverCalled();
yield expectNoObserverCalled();
yield closeStream();
}
@ -855,27 +941,22 @@ var gTests = [
desc: "'Always Allow' ignored and not shown on http pages",
run: function checkNoAlwaysOnHttp() {
// Load an http page instead of the https version.
let deferred = Promise.defer();
let browser = gBrowser.selectedBrowser;
browser.addEventListener("load", function onload() {
browser.removeEventListener("load", onload, true);
deferred.resolve();
}, true);
content.location = content.location.href.replace("https://", "http://");
yield deferred.promise;
browser.loadURI(browser.documentURI.spec.replace("https://", "http://"));
yield BrowserTestUtils.browserLoaded(browser);
// Initially set both permissions to 'allow'.
let Perms = Services.perms;
let uri = content.document.documentURIObject;
let uri = browser.documentURI;
Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
Perms.add(uri, "camera", Perms.ALLOW_ACTION);
// Request devices and expect a prompt despite the saved 'Allow' permission,
// because the connection isn't secure.
yield promisePopupNotificationShown("webRTC-shareDevices", () => {
content.wrappedJSObject.requestDevice(true, true);
});
expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
// Ensure that the 'Always Allow' action isn't shown.
let alwaysLabel = gNavigatorBundle.getString("getUserMedia.always.label");
@ -902,24 +983,27 @@ function test() {
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
tab.linkedBrowser.addEventListener("load", function onload() {
tab.linkedBrowser.removeEventListener("load", onload, true);
let browser = tab.linkedBrowser;
kObservedTopics.forEach(topic => {
Services.obs.addObserver(observer, topic, false);
});
Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true);
browser.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")();", true);
browser.addEventListener("load", function onload() {
browser.removeEventListener("load", onload, true);
is(PopupNotifications._currentNotifications.length, 0,
"should start the test without any prior popup notification");
Task.spawn(function () {
yield new Promise(resolve => SpecialPowers.pushPrefEnv({
"set": [[PREF_PERMISSION_FAKE, true]],
}, resolve));
for (let test of gTests) {
info(test.desc);
yield test.run();
// Cleanup before the next test
expectNoObserverCalled();
yield expectNoObserverCalled();
}
}).then(finish, ex => {
ok(false, "Unexpected Exception: " + ex);

View File

@ -1470,7 +1470,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
}
case "searchengine": {
[url, options.postData] =
this._parseAndRecordSearchEngineAction(action);
this.input._parseAndRecordSearchEngineAction(action);
break;
}
default: {

View File

@ -8,7 +8,7 @@ add_task(function* testTabSwitchContext() {
"page_action": {
"default_icon": "default.png",
"default_popup": "default.html",
"default_title": "Default Title",
"default_title": "Default Title \u263a",
},
"permissions": ["tabs"],
},
@ -17,10 +17,10 @@ add_task(function* testTabSwitchContext() {
let details = [
{ "icon": browser.runtime.getURL("default.png"),
"popup": browser.runtime.getURL("default.html"),
"title": "Default Title" },
"title": "Default Title \u263a" },
{ "icon": browser.runtime.getURL("1.png"),
"popup": browser.runtime.getURL("default.html"),
"title": "Default Title" },
"title": "Default Title \u263a" },
{ "icon": browser.runtime.getURL("2.png"),
"popup": browser.runtime.getURL("2.html"),
"title": "Title 2" },

View File

@ -61,7 +61,7 @@ function sorter(a, b) {
Object.defineProperty(FirefoxProfileMigrator.prototype, "sourceProfiles", {
get: function() {
return this._getAllProfiles().keys().map(x => ({id: x, name: x})).sort(sorter);
return [...this._getAllProfiles().keys()].map(x => ({id: x, name: x})).sort(sorter);
}
});

View File

@ -305,6 +305,19 @@ if (typeof Mozilla == 'undefined') {
pane: pane
});
};
/**
* Closes the tab where this code is running. As usual, if the tab is in the
* foreground, the tab that was displayed before is selected.
*
* The last tab in the current window will never be closed, in which case
* this call will have no effect. The calling code is expected to take an
* action after a small timeout in order to handle this case, for example by
* displaying a goodbye message or a button to restart the tour.
*/
Mozilla.UITour.closeTab = function() {
_sendEvent('closeTab');
};
})();
// Make this library Require-able.

View File

@ -741,6 +741,18 @@ this.UITour = {
});
break;
}
case "closeTab": {
// Find the <tabbrowser> element of the <browser> for which the event
// was generated originally. If the browser where the UI tour is loaded
// is windowless, just ignore the request to close the tab. The request
// is also ignored if this is the only tab in the window.
let tabBrowser = browser.ownerDocument.defaultView.gBrowser;
if (tabBrowser && tabBrowser.browsers.length > 1) {
tabBrowser.removeTab(tabBrowser.getTabForBrowser(browser));
}
break;
}
}
this.initForBrowser(browser, window);

View File

@ -7,6 +7,8 @@ support-files =
[browser_backgroundTab.js]
skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
[browser_closeTab.js]
skip-if = e10s # Bug 1073247 - UITour tests not e10s friendly
[browser_fxa.js]
skip-if = e10s || debug || asan # Bug 1073247 - UITour tests not e10s friendly # updateAppMenuItem leaks
[browser_no_tabs.js]

View File

@ -0,0 +1,23 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
var gTestTab;
var gContentAPI;
var gContentWindow;
function test() {
UITourTest();
}
var tests = [
taskify(function* test_closeTab() {
// Setting gTestTab to null indicates that the tab has already been closed,
// and if this does not happen the test run will fail.
gContentAPI.closeTab();
gTestTab = null;
}),
];

View File

@ -1,160 +0,0 @@
// Note: there are extra allowances for files used solely in Firefox desktop,
// see content/js/.eslintrc and modules/.eslintrc
{
"plugins": [
"react"
],
"ecmaFeatures": {
// Allow these.
"forOf": true,
"jsx": true,
// Disallow due to content.
"arrowFunctions": false,
"blockBindings": false,
"destructuring": false,
"generators": false,
"objectLiteralShorthandMethods": false,
"restParams": false,
"spread": false,
"templateStrings": false,
},
"env": {
"browser": true,
"es6": false,
"mocha": true,
},
"extends": "eslint:recommended",
"globals": {
"_": false,
"Backbone": false,
"chai": false,
"classNames": false,
"console": false,
"loop": true,
"LoopMochaUtils": true,
"MozActivity": false,
"RTCSessionDescription": false,
"OT": false,
"performance": false,
"Promise": false,
"React": false,
"sinon": false
},
"rules": {
// turn off all kinds of stuff that we actually do want, because
// right now, we're bootstrapping the linting infrastructure. We'll
// want to audit these rules, and start turning them on and fixing the
// problems they find, one at a time.
// Eslint built-in rules are documented at <http://eslint.org/docs/rules/>
"array-bracket-spacing": [2, "never"],
"block-spacing": [2, "always"],
"callback-return": 0, // TBD
"camelcase": 0, // TODO: set to 2
"comma-spacing": 2,
"comma-style": 2,
"computed-property-spacing": [2, "never"],
"consistent-return": 2,
"curly": [2, "all"],
"dot-location": [2, "property"],
"eol-last": 2,
"eqeqeq": [2, "smart"],
"generator-star-spacing": [2, {"before": false, "after": true}],
"jsx-quotes": [2, "prefer-double"],
"key-spacing": [2, {"beforeColon": false, "afterColon": true }],
"linebreak-style": [2, "unix"],
"new-cap": 0, // TODO: set to 2
"new-parens": 2,
"no-alert": 2,
"no-array-constructor": 2,
"no-caller": 2,
"no-catch-shadow": 2,
"no-class-assign": 2,
"no-const-assign": 2,
"no-console": 0, // Leave as 0. We use console logging in content code.
"no-duplicate-case": 2,
"no-else-return": 2,
"no-empty": 2,
"no-empty-label": 2,
"no-eval": 2,
"no-extend-native": 2, // XXX
"no-extra-bind": 0, // Leave as 0
"no-extra-parens": 0, // TODO: (bug?) [2, "functions"],
"no-extra-semi": 2,
"no-implied-eval": 2,
"no-invalid-this": 0, // TBD
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-multiple-empty-lines": 2,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-object": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-process-exit": 2,
"no-proto": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-undef-init": 2,
"no-underscore-dangle": 0, // Leave as 0. Commonly used for private variables.
"no-unexpected-multiline": 2,
"no-unneeded-ternary": 2,
"no-unused-expressions": 0, // TODO: Set to 2
"no-unused-vars": 0, // TODO: Set to 2
"no-use-before-define": 0, // TODO: Set to 2
"no-useless-call": 2,
"no-with": 2,
"object-curly-spacing": [2, "always"],
"operator-assignment": [2, "always"],
"quotes": [2, "double", "avoid-escape"],
"semi": 2,
"semi-spacing": [2, {"before": false, "after": true}],
"space-after-keywords": 2,
"space-before-blocks": 2,
"space-before-function-paren": [2, "never"],
"space-before-keywords": 2,
"space-infix-ops": 2,
"space-in-parens": [2, "never"],
"space-return-throw-case": 2,
"space-unary-ops": [2, {"words": true, "nonwords": false}],
"spaced-comment": [2, "always"],
"strict": [2, "function"],
"yoda": [2, "never"],
// eslint-plugin-react rules. These are documented at
// <https://github.com/yannickcr/eslint-plugin-react#list-of-supported-rules>
"react/jsx-curly-spacing": [2, "never"],
"react/jsx-no-bind": 2,
"react/jsx-no-duplicate-props": 2,
"react/jsx-no-undef": 2,
"react/jsx-sort-props": 2,
"react/jsx-sort-prop-types": 2,
"react/jsx-uses-vars": 2,
"react/no-did-mount-set-state": 2,
"react/no-did-update-set-state": 2,
"react/no-unknown-property": 2,
"react/prop-types": 2,
"react/self-closing-comp": 2,
"react/wrap-multilines": 2,
// Not worth it: React is defined globally
"react/jsx-uses-react": 0,
"react/react-in-jsx-scope": 0,
// These ones we don't want to ever enable
"react/display-name": 0,
"react/jsx-boolean-value": 0,
"react/no-danger": 0,
"react/no-multi-comp": 0
}
}

View File

@ -1,69 +0,0 @@
// This file defines additional items and rules specific for gecko files.
// This is applied on top of the basic .eslintrc for gecko specific directories
// e.g. the modules directory.
{
"ecmaFeatures": {
"arrowFunctions": true,
"blockBindings": true,
"destructuring": true,
"generators": true,
"objectLiteralShorthandMethods": true,
"restParams": true,
"spread": true,
"templateStrings": true,
},
"globals": {
// Gecko + Loop Globals.
"Chat": false,
"ChromeWorker": false,
"CommonUtils": false,
"Components": false,
"convertToRTCStatsReport": false,
"CustomizableUI": false,
"deriveHawkCredentials": false,
"eventEmitter": false,
"FxAccountsOAuthClient": false,
"FxAccountsProfileClient": false,
"gBrowser": false,
"gDNSService": false,
"gLoopBundle": false,
"gWM": false,
"HawkClient": false,
"hookWindowCloseForPanelClose": true,
"Iterator": false,
"Log": false,
"log": true,
"LOOP_SESSION_TYPE": true,
"LoopAPI": true,
"LoopCalls": true,
"loopCrypto": false,
"LoopRooms": true,
"LoopRoomsCache": true,
"MozLoopPushHandler": true,
"MozLoopService": true,
"OS": false,
"PrivateBrowsingUtils": false,
"roomsPushNotification": true,
"Services": false,
"Social": false,
"SocialShare": false,
"Task": false,
"UITour": false,
"WebChannel": false,
"XPCOMUtils": false,
"uuidgen": true,
// Test Related
"Assert": false,
},
"rules": {
"arrow-parens": 0, // TBD
"arrow-spacing": 2,
"eqeqeq": 0, // TBD
"generator-star-spacing": [2, "after"],
// We should fix the errors and enable this (set to 2)
"no-var": 0,
"prefer-arrow-callback": 0,// TODO: Set to 2.
"require-yield": 0, // TODO: Set to 2.
"strict": [2, "global"]
}
}

View File

@ -1,103 +1,3 @@
This is the directory for the Loop desktop implementation and the standalone
client.
This is the loop project output, https://github.com/mozilla/loop
The desktop implementation is the UX built into Firefox, activated by the Loop
button on the toolbar. The standalone client is the link-clicker UX for any
modern browser that supports WebRTC.
The standalone client is a set of web pages intended to be hosted on a
standalone server referenced by the loop-server.
The standalone client exists in standalone/ but shares items
(from content/shared/) with the desktop implementation. See the README.md
file in the standalone/ directory for how to run the server locally.
Working with React JSX files
============================
Our views use [React](http://facebook.github.io/react/) written in JSX files
and transpiled to JS before we commit. You need to install the JSX compiler
using npm in order to compile the .jsx files into regular .js ones:
npm install -g react-tools@0.12.2
Once installed, run build-jsx with the --watch option from
browser/extensions/loop, eg.:
cd browser/extensions/loop
./build-jsx --watch
build-jsx can also be do a one-time compile pass instead of watching if
the --watch argument is omitted. Be sure to commit any transpiled files
at the same time as changes to their sources.
Hacking
=======
Please be sure to execute
browser/extensions/loop/run-all-loop-tests.sh
from the top level before requesting review on a patch.
Linting
=======
run-all-loop-tests.sh will take care of this for you automatically, after
you've installed the dependencies by typing:
( cd standalone ; make install )
If you install eslint and the react plugin globally:
npm install -g eslint
npm install -g eslint-plugin-react
You can also run it by hand in the browser/extensions/loop directory:
eslint --ext .js --ext .jsx --ext .jsm .
Test coverage
=============
Initial setup
cd test
npm install
To run
npm run build-coverage
It will create a `coverage` folder under test/
Front-End Unit Tests
====================
The unit tests for Loop reside in three directories:
- test/desktop-local
- test/shared
- test/standalone
You can run these as part of the run-all-loop-tests.sh command above, or you can run these individually in Firefox. To run them individually, start the standalone client (see standalone/README.md) and load:
http://localhost:3000/test/
Functional Tests
================
These are currently a work in progress, but it's already possible to run a test
if you have a [loop-server](https://github.com/mozilla-services/loop-server)
install that is properly configured. From the top-level gecko directory,
execute:
export LOOP_SERVER=/Users/larry/src/loop-server
./mach marionette-test browser/extensions/loop/test/functional/manifest.ini
Once the automation is complete, we'll include this in run-all-loop-tests.sh
as well.
UI-Showcase
===========
This is a tool giving the layouts for all the frontend views of Loop, allowing debugging and testing of css layouts and local component behavior.
To access it, start the standalone client (see standalone/README.md) and load:
http://localhost:3000/ui/
Current extension version is: 0.1

View File

@ -3,6 +3,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* exported startup, shutdown, install, uninstall */
const { interfaces: Ci, utils: Cu, classes: Cc } = Components;
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@ -426,7 +428,7 @@ var WindowListener = {
}
let notification = new window.Notification(options.title, notificationOptions);
notification.addEventListener("click", e => {
notification.addEventListener("click", () => {
if (window.closed) {
return;
}
@ -483,6 +485,7 @@ var WindowListener = {
// Watch for title changes as opposed to location changes as more
// metadata about the page is available when this event fires.
gBrowser.addEventListener("DOMTitleChanged", this);
this._browserSharePaused = false;
}
this._maybeShowBrowserSharingInfoBar();
@ -504,6 +507,7 @@ var WindowListener = {
gBrowser.tabContainer.removeEventListener("TabSelect", this);
gBrowser.removeEventListener("DOMTitleChanged", this);
this._listeningToTabSelect = false;
this._browserSharePaused = false;
},
/**
@ -535,26 +539,37 @@ var WindowListener = {
}
let box = gBrowser.getNotificationBox();
let paused = false;
let pauseButtonLabel = this._getString(this._browserSharePaused ?
"infobar_button_resume_label" :
"infobar_button_pause_label");
let pauseButtonAccessKey = this._getString(this._browserSharePaused ?
"infobar_button_resume_accesskey" :
"infobar_button_pause_accesskey");
let barLabel = this._getString(this._browserSharePaused ?
"infobar_screenshare_paused_browser_message" :
"infobar_screenshare_browser_message2");
let bar = box.appendNotification(
this._getString("infobar_screenshare_browser_message2"),
barLabel,
kBrowserSharingNotificationId,
// Icon is defined in browser theme CSS.
null,
box.PRIORITY_WARNING_LOW,
[{
label: this._getString("infobar_button_pause_label"),
accessKey: this._getString("infobar_button_pause_accesskey"),
label: pauseButtonLabel,
accessKey: pauseButtonAccessKey,
isDefault: false,
callback: (event, buttonInfo, buttonNode) => {
paused = !paused;
bar.label = paused ? this._getString("infobar_screenshare_paused_browser_message") :
this._getString("infobar_screenshare_browser_message2");
bar.classList.toggle("paused", paused);
buttonNode.label = paused ? this._getString("infobar_button_resume_label") :
this._getString("infobar_button_pause_label");
buttonNode.accessKey = paused ? this._getString("infobar_button_resume_accesskey") :
this._getString("infobar_button_pause_accesskey");
this._browserSharePaused = !this._browserSharePaused;
bar.label = this._getString(this._browserSharePaused ?
"infobar_screenshare_paused_browser_message" :
"infobar_screenshare_browser_message2");
bar.classList.toggle("paused", this._browserSharePaused);
buttonNode.label = this._getString(this._browserSharePaused ?
"infobar_button_resume_label" :
"infobar_button_pause_label");
buttonNode.accessKey = this._getString(this._browserSharePaused ?
"infobar_button_resume_accesskey" :
"infobar_button_pause_accesskey");
return true;
},
type: "pause"
@ -571,6 +586,9 @@ var WindowListener = {
}]
);
// Sets 'paused' class if needed.
bar.classList.toggle("paused", !!this._browserSharePaused);
// Keep showing the notification bar until the user explicitly closes it.
bar.persistence = -1;
},
@ -612,7 +630,7 @@ var WindowListener = {
* Handles events from gBrowser.
*/
handleEvent: function(event) {
switch(event.type) {
switch (event.type) {
case "DOMTitleChanged":
// Get the new title of the shared tab
this._notifyBrowserSwitch();
@ -689,15 +707,10 @@ var WindowListener = {
window.LoopUI = LoopUI;
},
tearDownBrowserUI: function(window) {
let document = window.document;
tearDownBrowserUI: function() {
// Take any steps to remove UI or anything from the browser window
// document.getElementById() etc. will work here
if (window.LoopUI) {
window.LoopUI.removeMenuItem();
// XXX Add in tear-down of the panel.
}
// XXX Add in tear-down of the panel.
},
// nsIWindowMediatorListener functions.
@ -717,10 +730,10 @@ var WindowListener = {
}, false);
},
onCloseWindow: function(xulWindow) {
onCloseWindow: function() {
},
onWindowTitleChange: function(xulWindow, newTitle) {
onWindowTitleChange: function() {
}
};

View File

@ -1,173 +0,0 @@
#! /usr/bin/env python
import os
import sys
import re
from distutils import spawn
import subprocess
from threading import Thread
import argparse
def find_react_version(lib_dir):
"Finds the React library version number currently used."
for filename in os.listdir(lib_dir):
match = re.match(r"react-(.*)-prod\.js", filename)
if (match and match.group(1)):
return match.group(1)
print 'Unable to find the current react version used in content.'
print 'Please checked the %s directory.' % lib_dir
exit(1)
def append_arguments(array1, array2):
"Appends array2 onto the end of array1"
result = array1[:]
result.extend(array2)
return result
def check_jsx(jsx_path):
"Checks to see if jsx is installed or not"
if jsx_path is None:
print 'You do not have the react-tools installed'
print 'Please do $ npm install -g react-tools and make sure it is available in PATH'
exit(1)
def find_react_command():
"Searches for a jsx location and forms a runnable command"
if sys.platform != 'win32':
jsx_path = spawn.find_executable('jsx')
check_jsx(jsx_path)
return [jsx_path]
# Else windows.
def find_excutable_no_extension(fileName):
"""
spawn.find_executable assumes a '.exe' extension on windows
something which jsx doesn't have...
"""
paths = os.environ['PATH'].split(os.pathsep)
for path in paths:
file = os.path.join(path, fileName)
if os.path.isfile(file):
return path
return None
# jsx isn't a true windows executable, so the standard spawn
# processes get upset. Hence, we have to use node to run the
# jsx file direct.
node = spawn.find_executable('node')
if node is None:
print 'You do not have node installed, or it is not in your PATH'
exit(1)
# We need the jsx path to make node happy
jsx_path = find_excutable_no_extension('jsx')
check_jsx(jsx_path)
# This is what node really wants to run.
jsx_path = os.path.join(jsx_path,
"node_modules", "react-tools", "bin", "jsx")
return [node, jsx_path]
SHARED_LIBS_DIR=os.path.join(os.path.dirname(__file__), "content", "shared", "vendor")
REACT_VERSION=find_react_version(SHARED_LIBS_DIR)
src_files = [] # files to be compiled
run_command = find_react_command()
if sys.platform == 'win32':
print 'Please ensure you are running react-tools version %s' % REACT_VERSION
print 'You may be already, but we are not currently able to detect it'
else:
p = subprocess.Popen(append_arguments(run_command, ['-V']),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
for line in iter(p.stdout.readline, b''):
info = line.rstrip()
if not info == REACT_VERSION:
print 'You have the wrong version of react-tools installed'
print 'Please use version %s' % REACT_VERSION
exit(1)
# parse the CLI arguments
description = 'Loop build tool for JSX files. ' + \
'Will scan entire loop directory and compile them in place. ' + \
'Must be executed from browser/extensions/loop directory.'
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--watch', '-w', action='store_true', help='continuous' +
'build based on file changes (optional)')
args = parser.parse_args()
# loop through all tuples and get unique dirs only
unique_jsx_dirs = []
# find all .jsx files
for dirname, dirnames, filenames in os.walk('.'):
for filename in filenames:
if '.jsx' == os.path.splitext(filename)[1]: # (root, ext)
src_files.append(filename)
if dirname not in unique_jsx_dirs:
unique_jsx_dirs.append(dirname)
def jsx_run_watcher(path):
# keep --target=es3 for now, as at least our UnsupportedBrowserView wants
# to be able to render on IE8
subprocess.call(append_arguments(run_command, ['--target=es3', '-w', '-x',
'jsx', path, path]))
def start_jsx_watcher_threads(dirs):
"""
starts a thread with a jsx watch process
for every dir in the dirs argument
"""
threads = []
for udir in dirs:
t = Thread(target=jsx_run_watcher, args=(udir,))
threads.append(t)
t.start()
for t in threads:
t.join()
def check_file_packaging(srcs):
"""
get all lines from jar.mn file
check against the files we are going to compile.
Note that this only looks at filenames, and will miss
the one of two identically named files.
"""
# get all lines from jar.mn
packaged_files = [line.strip() for line in open('./jar.mn')]
# loop through our compiled files and compare against jar.mn
# XXX fix to use paths so that identically named files don't get overlooked
# and update the docstring for this function
missing_jar_files = [] # initially empty
for src_file in srcs:
transpiled_file = os.path.splitext(src_file)[0] + ".js" # (root, ext)
if not any(transpiled_file in x for x in packaged_files):
missing_jar_files.append(transpiled_file)
# output a message to the user
if len(missing_jar_files):
for f in missing_jar_files:
print f + ' not in jar.mn file'
check_file_packaging(src_files)
if args.watch:
start_jsx_watcher_threads(unique_jsx_dirs)
else:
for d in unique_jsx_dirs:
out = subprocess.call(append_arguments(run_command, ['-x', 'jsx', d, d]))

View File

@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const { utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
@ -35,6 +35,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "loopCrypto",
XPCOMUtils.defineLazyModuleGetter(this, "ObjectUtils",
"resource://gre/modules/ObjectUtils.jsm");
/* exported LoopRooms, roomsPushNotification */
this.EXPORTED_SYMBOLS = ["LoopRooms", "roomsPushNotification"];
@ -737,7 +738,7 @@ var LoopRoomsInternal = {
let room = this.rooms.get(roomToken);
let url = "/rooms/" + encodeURIComponent(roomToken);
MozLoopService.hawkRequest(this.sessionType, url, "DELETE")
.then(response => {
.then(() => {
this.rooms.delete(roomToken);
eventEmitter.emit("delete", room);
eventEmitter.emit("delete:" + room.roomToken, room);

View File

@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const { utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");

View File

@ -394,7 +394,9 @@ const kMessageHandlers = {
version: appInfo.version,
OS: appInfo.OS
};
} catch (ex) {}
} catch (ex) {
// Do nothing
}
}
reply(gAppVersionInfo);
},
@ -415,7 +417,7 @@ const kMessageHandlers = {
let name = message.data[0];
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
let url = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
let url = `chrome://loop/content/shared/sounds/${name}.ogg`;
request.open("GET", url, true);
request.responseType = "arraybuffer";
@ -425,7 +427,7 @@ const kMessageHandlers = {
return;
}
let blob = new Blob([request.response], {type: "audio/ogg"});
let blob = new Blob([request.response], { type: "audio/ogg" });
reply(blob);
};
@ -592,7 +594,7 @@ const kMessageHandlers = {
win.messageManager.removeMessageListener("PageMetadata:PageDataResult", onPageDataResult);
let pageData = msg.json;
win.LoopUI.getFavicon(function(err, favicon) {
if (err) {
if (err && err !== "favicon not found for uri") {
MozLoopService.log.error("Error occurred whilst fetching favicon", err);
// We don't return here intentionally to make sure the callback is
// invoked at all times. We just report the error here.
@ -624,49 +626,6 @@ const kMessageHandlers = {
reply(gSocialProviders);
},
/**
* Compose a URL pointing to the location of an avatar by email address.
* At the moment we use the Gravatar service to match email addresses with
* avatars. If no email address is found we return null.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [
* {String} emailAddress Users' email address
* {Number} size Size of the avatar image
* to return in pixels. Optional.
* Default value: 40.
* ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
* @return the URL pointing to an avatar matching the provided email address
* or null if this is not available.
*/
GetUserAvatar: function(message, reply) {
let [emailAddress, size] = message.data;
if (!emailAddress || !MozLoopService.getLoopPref("contacts.gravatars.show")) {
reply(null);
return;
}
// Do the MD5 dance.
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(Ci.nsICryptoHash.MD5);
let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stringStream.data = emailAddress.trim().toLowerCase();
hasher.updateFromStream(stringStream, -1);
let hash = hasher.finish(false);
// Convert the binary hash data to a hex string.
let md5Email = Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
// Compose the Gravatar URL.
reply("https://www.gravatar.com/avatar/" + md5Email +
".jpg?default=blank&s=" + (size || 40));
},
/**
* Gets an object with data that represents the currently
* authenticated user's identity.
@ -1043,58 +1002,6 @@ const kMessageHandlers = {
reply();
},
/**
* Starts alerting the user about an incoming call
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
StartAlerting: function(message, reply) {
let chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
chromeWindow.getAttention();
ringer = new chromeWindow.Audio();
ringer.src = Services.prefs.getCharPref("loop.ringtone");
ringer.loop = true;
ringer.load();
ringer.play();
targetWindow.document.addEventListener("visibilitychange",
ringerStopper = function(event) {
if (event.currentTarget.hidden) {
kMessageHandlers.StopAlerting();
}
});
reply();
},
/**
* Stops alerting the user about an incoming call
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
StopAlerting: function(message, reply) {
if (!ringer) {
reply();
return;
}
if (ringerStopper) {
ringer.ownerDocument.removeEventListener("visibilitychange",
ringerStopper);
ringerStopper = null;
}
ringer.pause();
ringer = null;
reply();
},
/**
* Adds a value to a telemetry histogram.
*
@ -1167,7 +1074,7 @@ const LoopAPIInternal = {
} catch (ex) {
MozLoopService.log.error("Failed to send reply back to content:", ex);
}
}
};
}
// First, check if this is a batch call.
@ -1214,7 +1121,7 @@ const LoopAPIInternal = {
* It iterates over all the messages, sends each to their appropriate handler
* and collects their results. The results will be sent back in one go as response
* to the batch message.
*
*
* @param {Number} seq Sequence ID of this message
* @param {Object} message Message containing the following parameters in
* its `data` property:
@ -1289,7 +1196,7 @@ const LoopAPIInternal = {
try {
message.target.sendAsyncMessage(kPushMessageName, [pushMessagePrefix +
prettyEventName, data]);
} catch(ex) {
} catch (ex) {
MozLoopService.log.debug("Unable to send event through to target: " +
ex.message);
// Unregister event handlers when the message port is unreachable.
@ -1335,9 +1242,12 @@ const LoopAPIInternal = {
for (let page of gPageListeners) {
try {
page.sendAsyncMessage(kPushMessageName, [name, data]);
} catch (ex if ex.result == Components.results.NS_ERROR_NOT_INITIALIZED) {
// Don't make noise when the Remote Page Manager needs more time to
} catch (ex) {
// Only make noise when the Remote Page Manager needs more time to
// initialize.
if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED) {
throw ex;
}
}
}
},
@ -1349,7 +1259,9 @@ const LoopAPIInternal = {
if (!gPageListeners) {
return;
}
[for (listener of gPageListeners) listener.destroy()];
for (let listener of gPageListeners) {
listener.destroy();
}
gPageListeners = null;
// Unsubscribe from global events.

View File

@ -13,6 +13,8 @@ Cu.import("resource://gre/modules/Timer.jsm");
const { MozLoopService } = Cu.import("chrome://loop/content/modules/MozLoopService.jsm", {});
const consoleLog = MozLoopService.log;
/* exported MozLoopPushHandler */
this.EXPORTED_SYMBOLS = ["MozLoopPushHandler"];
const CONNECTION_STATE_CLOSED = 0;
@ -556,7 +558,7 @@ var MozLoopPushHandler = {
* This method will continually try to re-establish a connection
* to the PushServer unless shutdown has been called.
*/
_onClose: function(aCode, aReason) {
_onClose: function(aCode) {
this._pingMonitor.stop();
switch (this.connectionState) {

View File

@ -4,11 +4,7 @@
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
// Invalid auth token as per
// https://github.com/mozilla-services/loop-server/blob/45787d34108e2f0d87d74d4ddf4ff0dbab23501c/loop/errno.json#L6
const INVALID_AUTH_TOKEN = 110;
const { interfaces: Ci, utils: Cu, results: Cr } = Components;
const LOOP_SESSION_TYPE = {
GUEST: 1,
@ -201,7 +197,6 @@ var gFxAEnabled = true;
var gFxAOAuthClientPromise = null;
var gFxAOAuthClient = null;
var gErrors = new Map();
var gLastWindowId = 0;
var gConversationWindowData = new Map();
/**
@ -1676,8 +1671,6 @@ this.MozLoopService = {
return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
}).then(tokenData => {
MozLoopServiceInternal.fxAOAuthTokenData = tokenData;
return tokenData;
}).then(tokenData => {
return MozLoopServiceInternal.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA).then(() => {
MozLoopServiceInternal.clearError("login");
MozLoopServiceInternal.clearError("profile");
@ -1768,6 +1761,13 @@ this.MozLoopService = {
return;
}
let url = new URL("/settings", fxAOAuthClient.parameters.content_uri);
if (this.userProfile) {
// fxA User profile is present, open settings for the correct profile. Bug: 1070208
let fxAProfileUid = MozLoopService.userProfile.uid;
url = new URL("/settings?uid=" + fxAProfileUid, fxAOAuthClient.parameters.content_uri);
}
let win = Services.wm.getMostRecentWindow("navigator:browser");
win.switchToTabHavingURI(url.toString(), true);
} catch (ex) {
@ -1865,7 +1865,7 @@ this.MozLoopService = {
*/
openURL: function(url) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
win.openUILinkIn(url, "tab");
win.openUILinkIn(Services.urlFormatter.formatURL(url), "tab");
},
/**

View File

@ -10,7 +10,6 @@
importScripts("resource://gre/modules/osfile.jsm");
var File = OS.File;
var Encoder = new TextEncoder();
var Counter = 0;
@ -49,13 +48,20 @@ onmessage = function(e) {
// Save to disk
let array = Encoder.encode(pingStr);
try {
File.makeDir(directory,
{ unixMode: OS.Constants.S_IRWXU, ignoreExisting: true });
File.writeAtomic(OS.Path.join(directory, filename), array);
OS.File.makeDir(directory, {
unixMode: OS.Constants.S_IRWXU,
ignoreExisting: true
});
OS.File.writeAtomic(OS.Path.join(directory, filename), array);
postMessage({ ok: true });
} catch (ex if ex instanceof File.Error) {
} catch (ex) {
// Instances of OS.File.Error know how to serialize themselves
postMessage({fail: File.Error.toMsg(ex)});
if (ex instanceof OS.File.Error) {
postMessage({ fail: OS.File.Error.toMsg(ex) });
}
else {
throw ex;
}
}
};

View File

@ -21,10 +21,10 @@
<script type="text/javascript" src="panels/vendor/l10n.js"></script>
<script type="text/javascript" src="panels/js/otconfig.js"></script>
<script type="text/javascript" src="shared/vendor/sdk.js"></script>
<script type="text/javascript" src="shared/vendor/react-0.13.3.js"></script>
<script type="text/javascript" src="shared/vendor/lodash-3.9.3.js"></script>
<script type="text/javascript" src="shared/vendor/backbone-1.2.1.js"></script>
<script type="text/javascript" src="shared/vendor/classnames-2.2.0.js"></script>
<script type="text/javascript" src="shared/vendor/react.js"></script>
<script type="text/javascript" src="shared/vendor/lodash.js"></script>
<script type="text/javascript" src="shared/vendor/backbone.js"></script>
<script type="text/javascript" src="shared/vendor/classnames.js"></script>
<script type="text/javascript" src="shared/js/loopapi-client.js"></script>
<script type="text/javascript" src="shared/js/utils.js"></script>

View File

@ -147,35 +147,6 @@ body {
/* Rooms CSS */
.no-conversations-message {
/* example of vertical aligning a container in an element see:
http://zerosixthree.se/vertical-align-anything-with-just-3-lines-of-css/ */
text-align: center;
color: #4a4a4a;
font-weight: lighter;
position: relative;
top: 50%;
transform: translateY(-50%);
padding-top: 11rem;
padding-bottom: 1rem;
background-image: url("../../shared/img/empty_conversations.svg");
background-repeat: no-repeat;
background-position: top center;
}
.panel-text-medium,
.panel-text-large {
margin: 3px 0;
}
.panel-text-medium {
font-size: 1.6rem;
}
.panel-text-large {
font-size: 2.2rem;
}
.room-list-loading {
position: relative;
text-align: center;
@ -203,18 +174,20 @@ body {
.rooms > h1 {
color: #666;
font-size: 1rem;
padding: .5rem 0;
padding: .5rem 15px;
height: 3rem;
line-height: 3rem;
margin: 0 15px;
}
.new-room-view {
border-bottom: 1px solid #d8d8d8;
display: flex;
flex-direction: column;
}
.new-room-view + h1 {
border-top: 1px solid #d8d8d8;
}
.new-room-view > .btn {
border-radius: 5px;
font-size: 1.2rem;
@ -245,15 +218,6 @@ body {
width: 100%;
}
.room-list-empty {
border-bottom-width: 0;
flex: 1;
/* the child no-conversations-message is vertical aligned inside this container
see: http://zerosixthree.se/vertical-align-anything-with-just-3-lines-of-css/
stops blurring from decimal pixels being rendered - pixel rounding */
transform-style: preserve-3d;
}
.room-list > .room-entry {
padding: .2rem 15px;
/* Always show the default pointer, even over the text part of the entry. */
@ -569,9 +533,8 @@ html[dir="rtl"] .generate-url-spinner {
/* Sign in/up link */
.signin-link {
flex: 2 1 auto;
flex: 1;
margin: 0;
text-align: right;
}
.signin-link > a {
@ -597,15 +560,6 @@ html[dir="rtl"] .generate-url-spinner {
-moz-margin-start: .5em;
}
.user-details .dropdown-menu {
bottom: 1.3rem; /* Just above the text. */
left: -5px; /* Compensate for button padding. */
}
html[dir="rtl"] .user-details .dropdown-menu {
right: -5px;
}
.settings-menu .dropdown-menu {
/* The panel can't have dropdown menu overflowing its iframe boudaries;
let's anchor it from the bottom-right, while resetting the top & left values
@ -646,15 +600,18 @@ html[dir="rtl"] .settings-menu .dropdown-menu {
height: 42px;
}
.footer .signin-details {
.footer > .signin-details {
align-items: center;
display: flex;
}
.footer .user-identity {
.footer > .user-identity {
flex: 1;
color: #000;
font-weight: bold;
margin: 0;
margin-inline-end: 1rem;
overflow-x: hidden;
text-overflow: ellipsis;
}
/* First time use */

View File

@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.conversation = (function(mozL10n) {
loop.conversation = function (mozL10n) {
"use strict";
var sharedMixins = loop.shared.mixins;
@ -19,40 +19,36 @@ loop.conversation = (function(mozL10n) {
* in progress, and hence, which view to display.
*/
var AppControllerView = React.createClass({
mixins: [
Backbone.Events,
loop.store.StoreMixin("conversationAppStore"),
sharedMixins.DocumentTitleMixin,
sharedMixins.WindowCloseMixin
],
displayName: "AppControllerView",
mixins: [Backbone.Events, loop.store.StoreMixin("conversationAppStore"), sharedMixins.DocumentTitleMixin, sharedMixins.WindowCloseMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
},
getInitialState: function() {
getInitialState: function () {
return this.getStoreState();
},
_renderFeedbackForm: function() {
_renderFeedbackForm: function () {
this.setTitle(mozL10n.get("conversation_has_ended"));
return (<FeedbackView
onAfterFeedbackReceived={this.closeWindow} />);
return React.createElement(FeedbackView, {
onAfterFeedbackReceived: this.closeWindow });
},
/**
* We only show the feedback for once every 6 months, otherwise close
* the window.
*/
handleCallTerminated: function() {
handleCallTerminated: function () {
var delta = new Date() - new Date(this.state.feedbackTimestamp);
// Show timestamp if feedback period (6 months) passed.
// 0 is default value for pref. Always show feedback form on first use.
if (this.state.feedbackTimestamp === 0 ||
delta >= this.state.feedbackPeriod) {
if (this.state.feedbackTimestamp === 0 || delta >= this.state.feedbackPeriod) {
this.props.dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
return;
}
@ -60,29 +56,32 @@ loop.conversation = (function(mozL10n) {
this.closeWindow();
},
render: function() {
render: function () {
if (this.state.showFeedbackForm) {
return this._renderFeedbackForm();
}
switch (this.state.windowType) {
case "room": {
return (<DesktopRoomConversationView
chatWindowDetached={this.state.chatWindowDetached}
dispatcher={this.props.dispatcher}
onCallTerminated={this.handleCallTerminated}
roomStore={this.props.roomStore} />);
}
case "failed": {
return (<RoomFailureView
dispatcher={this.props.dispatcher}
failureReason={FAILURE_DETAILS.UNKNOWN} />);
}
default: {
// If we don't have a windowType, we don't know what we are yet,
// so don't display anything.
return null;
}
case "room":
{
return React.createElement(DesktopRoomConversationView, {
chatWindowDetached: this.state.chatWindowDetached,
dispatcher: this.props.dispatcher,
onCallTerminated: this.handleCallTerminated,
roomStore: this.props.roomStore });
}
case "failed":
{
return React.createElement(RoomFailureView, {
dispatcher: this.props.dispatcher,
failureReason: FAILURE_DETAILS.UNKNOWN });
}
default:
{
// If we don't have a windowType, we don't know what we are yet,
// so don't display anything.
return null;
}
}
}
});
@ -100,20 +99,10 @@ loop.conversation = (function(mozL10n) {
windowId = hash[1];
}
var requests = [
["GetAllConstants"],
["GetAllStrings"],
["GetLocale"],
["GetLoopPref", "ot.guid"],
["GetLoopPref", "textChat.enabled"],
["GetLoopPref", "feedback.periodSec"],
["GetLoopPref", "feedback.dateLastSeenSec"]
];
var prefetch = [
["GetConversationWindowData", windowId]
];
var requests = [["GetAllConstants"], ["GetAllStrings"], ["GetLocale"], ["GetLoopPref", "ot.guid"], ["GetLoopPref", "textChat.enabled"], ["GetLoopPref", "feedback.periodSec"], ["GetLoopPref", "feedback.dateLastSeenSec"]];
var prefetch = [["GetConversationWindowData", windowId]];
return loop.requestMulti.apply(null, requests.concat(prefetch)).then(function(results) {
return loop.requestMulti.apply(null, requests.concat(prefetch)).then(function (results) {
// `requestIdx` is keyed off the order of the `requests` and `prefetch`
// arrays. Be careful to update both when making changes.
var requestIdx = 0;
@ -124,7 +113,7 @@ loop.conversation = (function(mozL10n) {
var locale = results[++requestIdx];
mozL10n.initialize({
locale: locale,
getStrings: function(key) {
getStrings: function (key) {
if (!(key in stringBundle)) {
console.error("No string found for key: ", key);
return "{ textContent: '' }";
@ -138,10 +127,10 @@ loop.conversation = (function(mozL10n) {
// don't work in the conversation window
var currGuid = results[++requestIdx];
window.OT.overrideGuidStorage({
get: function(callback) {
get: function (callback) {
callback(null, currGuid);
},
set: function(guid, callback) {
set: function (guid, callback) {
// See nsIPrefBranch
var PREF_STRING = 32;
currGuid = guid;
@ -177,7 +166,7 @@ loop.conversation = (function(mozL10n) {
feedbackTimestamp: results[++requestIdx]
});
prefetch.forEach(function(req) {
prefetch.forEach(function (req) {
req.shift();
loop.storeRequest(req, results[++requestIdx]);
});
@ -195,13 +184,12 @@ loop.conversation = (function(mozL10n) {
textChatStore: textChatStore
});
React.render(
<AppControllerView
dispatcher={dispatcher}
roomStore={roomStore} />, document.querySelector("#main"));
React.render(React.createElement(AppControllerView, {
dispatcher: dispatcher,
roomStore: roomStore }), document.querySelector("#main"));
document.documentElement.setAttribute("lang", mozL10n.getLanguage());
document.documentElement.setAttribute("dir", mozL10n.getDirection());
document.documentElement.setAttribute("lang", mozL10n.language.code);
document.documentElement.setAttribute("dir", mozL10n.language.direction);
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
dispatcher.dispatch(new sharedActions.GetWindowData({
@ -222,6 +210,6 @@ loop.conversation = (function(mozL10n) {
*/
_sdkDriver: null
};
})(document.mozL10n);
}(document.mozL10n);
document.addEventListener("DOMContentLoaded", loop.conversation.init);

View File

@ -3,14 +3,17 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.feedbackViews = (function(_, mozL10n) {
loop.feedbackViews = function (_, mozL10n) {
"use strict";
/**
* Feedback view is displayed once every 6 months (loop.feedback.periodSec)
* after a conversation has ended.
*/
var FeedbackView = React.createClass({
displayName: "FeedbackView",
propTypes: {
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
@ -19,26 +22,32 @@ loop.feedbackViews = (function(_, mozL10n) {
* Pressing the button to leave feedback will open the form in a new page
* and close the conversation window.
*/
onFeedbackButtonClick: function() {
loop.request("GetLoopPref", "feedback.formURL").then(function(url) {
onFeedbackButtonClick: function () {
loop.request("GetLoopPref", "feedback.formURL").then(function (url) {
loop.request("OpenURL", url).then(this.props.onAfterFeedbackReceived);
}.bind(this));
},
render: function() {
return (
<div className="feedback-view-container">
<h2 className="feedback-heading">
{mozL10n.get("feedback_window_heading")}
</h2>
<div className="feedback-hello-logo" />
<div className="feedback-button-container">
<button onClick={this.onFeedbackButtonClick}
ref="feedbackFormBtn">
{mozL10n.get("feedback_request_button")}
</button>
</div>
</div>
render: function () {
return React.createElement(
"div",
{ className: "feedback-view-container" },
React.createElement(
"h2",
{ className: "feedback-heading" },
mozL10n.get("feedback_window_heading")
),
React.createElement("div", { className: "feedback-hello-logo" }),
React.createElement(
"div",
{ className: "feedback-button-container" },
React.createElement(
"button",
{ onClick: this.onFeedbackButtonClick,
ref: "feedbackFormBtn" },
mozL10n.get("feedback_request_button")
)
)
);
}
});
@ -46,4 +55,4 @@ loop.feedbackViews = (function(_, mozL10n) {
return {
FeedbackView: FeedbackView
};
})(_, navigator.mozL10n || document.mozL10n);
}(_, navigator.mozL10n || document.mozL10n);

View File

@ -260,9 +260,11 @@ loop.store = loop.store || {};
this._notifications.remove("create-room-error");
loop.request("Rooms:Create", roomCreationData).then(function(result) {
var buckets = this._constants.ROOM_CREATE;
if (result && result.isError) {
if (!result || result.isError) {
loop.request("TelemetryAddValue", "LOOP_ROOM_CREATE", buckets.CREATE_FAIL);
this.dispatchAction(new sharedActions.CreateRoomError({ error: result }));
this.dispatchAction(new sharedActions.CreateRoomError({
error: result ? result : new Error("no result")
}));
return;
}

View File

@ -0,0 +1,634 @@
/* 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/. */
var loop = loop || {};
loop.roomViews = function (mozL10n) {
"use strict";
var ROOM_STATES = loop.store.ROOM_STATES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
/**
* ActiveRoomStore mixin.
* @type {Object}
*/
var ActiveRoomStoreMixin = {
mixins: [Backbone.Events],
propTypes: {
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
},
componentWillMount: function () {
this.listenTo(this.props.roomStore, "change:activeRoom", this._onActiveRoomStateChanged);
this.listenTo(this.props.roomStore, "change:error", this._onRoomError);
this.listenTo(this.props.roomStore, "change:savingContext", this._onRoomSavingContext);
},
componentWillUnmount: function () {
this.stopListening(this.props.roomStore);
},
_onActiveRoomStateChanged: function () {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState(this.props.roomStore.getStoreState("activeRoom"));
}
},
_onRoomError: function () {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState({ error: this.props.roomStore.getStoreState("error") });
}
},
_onRoomSavingContext: function () {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState({ savingContext: this.props.roomStore.getStoreState("savingContext") });
}
},
getInitialState: function () {
var storeState = this.props.roomStore.getStoreState("activeRoom");
return _.extend({
// Used by the UI showcase.
roomState: this.props.roomState || storeState.roomState,
savingContext: false
}, storeState);
}
};
/**
* Used to display errors in direct calls and rooms to the user.
*/
var FailureInfoView = React.createClass({
displayName: "FailureInfoView",
propTypes: {
failureReason: React.PropTypes.string.isRequired
},
/**
* Returns the translated message appropraite to the failure reason.
*
* @return {String} The translated message for the failure reason.
*/
_getMessage: function () {
switch (this.props.failureReason) {
case FAILURE_DETAILS.NO_MEDIA:
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
return mozL10n.get("no_media_failure_message");
case FAILURE_DETAILS.TOS_FAILURE:
return mozL10n.get("tos_failure_message", { clientShortname: mozL10n.get("clientShortname2") });
case FAILURE_DETAILS.ICE_FAILED:
return mozL10n.get("ice_failure_message");
default:
return mozL10n.get("generic_failure_message");
}
},
render: function () {
return React.createElement(
"div",
{ className: "failure-info" },
React.createElement("div", { className: "failure-info-logo" }),
React.createElement(
"h2",
{ className: "failure-info-message" },
this._getMessage()
)
);
}
});
/**
* Something went wrong view. Displayed when there's a big problem.
*/
var RoomFailureView = React.createClass({
displayName: "RoomFailureView",
mixins: [sharedMixins.AudioMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
failureReason: React.PropTypes.string
},
componentDidMount: function () {
this.play("failure");
},
handleRejoinCall: function () {
this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
},
render: function () {
var btnTitle;
if (this.props.failureReason === FAILURE_DETAILS.ICE_FAILED) {
btnTitle = mozL10n.get("retry_call_button");
} else {
btnTitle = mozL10n.get("rejoin_button");
}
return React.createElement(
"div",
{ className: "room-failure" },
React.createElement(FailureInfoView, { failureReason: this.props.failureReason }),
React.createElement(
"div",
{ className: "btn-group call-action-group" },
React.createElement(
"button",
{ className: "btn btn-info btn-rejoin",
onClick: this.handleRejoinCall },
btnTitle
)
)
);
}
});
var SocialShareDropdown = React.createClass({
displayName: "SocialShareDropdown",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomUrl: React.PropTypes.string,
show: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array
},
handleAddServiceClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
},
handleProviderClick: function (event) {
event.preventDefault();
var origin = event.currentTarget.dataset.provider;
var provider = this.props.socialShareProviders.filter(function (socialProvider) {
return socialProvider.origin === origin;
})[0];
this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
provider: provider,
roomUrl: this.props.roomUrl,
previews: []
}));
},
render: function () {
// Don't render a thing when no data has been fetched yet.
if (!this.props.socialShareProviders) {
return null;
}
var cx = classNames;
var shareDropdown = cx({
"share-service-dropdown": true,
"dropdown-menu": true,
"visually-hidden": true,
"hide": !this.props.show
});
return React.createElement(
"ul",
{ className: shareDropdown },
React.createElement(
"li",
{ className: "dropdown-menu-item", onClick: this.handleAddServiceClick },
React.createElement("i", { className: "icon icon-add-share-service" }),
React.createElement(
"span",
null,
mozL10n.get("share_add_service_button")
)
),
this.props.socialShareProviders.length ? React.createElement("li", { className: "dropdown-menu-separator" }) : null,
this.props.socialShareProviders.map(function (provider, idx) {
return React.createElement(
"li",
{ className: "dropdown-menu-item",
"data-provider": provider.origin,
key: "provider-" + idx,
onClick: this.handleProviderClick },
React.createElement("img", { className: "icon", src: provider.iconURL }),
React.createElement(
"span",
null,
provider.name
)
);
}.bind(this))
);
}
});
/**
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({
displayName: "DesktopRoomInvitationView",
statics: {
TRIGGERED_RESET_DELAY: 2000
},
mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array
},
getInitialState: function () {
return {
copiedUrl: false,
newRoomName: ""
};
},
handleEmailButtonClick: function (event) {
event.preventDefault();
var roomData = this.props.roomData;
var contextURL = roomData.roomContextUrls && roomData.roomContextUrls[0];
if (contextURL) {
if (contextURL.location === null) {
contextURL = undefined;
} else {
contextURL = sharedUtils.formatURL(contextURL.location).hostname;
}
}
this.props.dispatcher.dispatch(new sharedActions.EmailRoomUrl({
roomUrl: roomData.roomUrl,
roomDescription: contextURL,
from: "conversation"
}));
},
handleFacebookButtonClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.FacebookShareRoomUrl({
from: "conversation",
roomUrl: this.props.roomData.roomUrl
}));
},
handleCopyButtonClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
roomUrl: this.props.roomData.roomUrl,
from: "conversation"
}));
this.setState({ copiedUrl: true });
setTimeout(this.resetTriggeredButtons, this.constructor.TRIGGERED_RESET_DELAY);
},
/**
* Reset state of triggered buttons if necessary
*/
resetTriggeredButtons: function () {
if (this.state.copiedUrl) {
this.setState({ copiedUrl: false });
}
},
render: function () {
if (!this.props.show || !this.props.roomData.roomUrl) {
return null;
}
var cx = classNames;
return React.createElement(
"div",
{ className: "room-invitation-overlay" },
React.createElement(
"div",
{ className: "room-invitation-content" },
React.createElement(
"p",
null,
React.createElement(
"span",
{ className: "room-context-header" },
mozL10n.get("invite_header_text_bold")
),
" ",
React.createElement(
"span",
null,
mozL10n.get("invite_header_text3")
)
)
),
React.createElement(
"div",
{ className: cx({
"btn-group": true,
"call-action-group": true
}) },
React.createElement(
"div",
{ className: cx({
"btn-copy": true,
"invite-button": true,
"triggered": this.state.copiedUrl
}),
onClick: this.handleCopyButtonClick },
React.createElement("img", { src: "shared/img/glyph-link-16x16.svg" }),
React.createElement(
"p",
null,
mozL10n.get(this.state.copiedUrl ? "invite_copied_link_button" : "invite_copy_link_button")
)
),
React.createElement(
"div",
{ className: "btn-email invite-button",
onClick: this.handleEmailButtonClick,
onMouseOver: this.resetTriggeredButtons },
React.createElement("img", { src: "shared/img/glyph-email-16x16.svg" }),
React.createElement(
"p",
null,
mozL10n.get("invite_email_link_button")
)
),
React.createElement(
"div",
{ className: "btn-facebook invite-button",
onClick: this.handleFacebookButtonClick,
onMouseOver: this.resetTriggeredButtons },
React.createElement("img", { src: "shared/img/glyph-facebook-16x16.svg" }),
React.createElement(
"p",
null,
mozL10n.get("invite_facebook_button3")
)
)
),
React.createElement(SocialShareDropdown, {
dispatcher: this.props.dispatcher,
ref: "menu",
roomUrl: this.props.roomData.roomUrl,
show: this.state.showMenu,
socialShareProviders: this.props.socialShareProviders })
);
}
});
/**
* Desktop room conversation view.
*/
var DesktopRoomConversationView = React.createClass({
displayName: "DesktopRoomConversationView",
mixins: [ActiveRoomStoreMixin, sharedMixins.DocumentTitleMixin, sharedMixins.MediaSetupMixin, sharedMixins.RoomsAudioMixin, sharedMixins.WindowCloseMixin],
propTypes: {
chatWindowDetached: React.PropTypes.bool.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
onCallTerminated: React.PropTypes.func.isRequired,
remotePosterUrl: React.PropTypes.string,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
},
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
// the sdk wouldn't need to know this, but we can't change that.
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT && nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: !this.state.videoMuted
})
}));
}
// Automatically start sharing a tab now we're ready to share.
if (this.state.roomState !== ROOM_STATES.SESSION_CONNECTED && nextState.roomState === ROOM_STATES.SESSION_CONNECTED) {
this.props.dispatcher.dispatch(new sharedActions.StartBrowserShare());
}
},
/**
* User clicked on the "Leave" button.
*/
leaveRoom: function () {
if (this.state.used) {
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
} else {
this.closeWindow();
}
},
/**
* Used to control publishing a stream - i.e. to mute a stream
*
* @param {String} type The type of stream, e.g. "audio" or "video".
* @param {Boolean} enabled True to enable the stream, false otherwise.
*/
publishStream: function (type, enabled) {
this.props.dispatcher.dispatch(new sharedActions.SetMute({
type: type,
enabled: enabled
}));
},
/**
* Determine if the invitation controls should be shown.
*
* @return {Boolean} True if there's no guests.
*/
_shouldRenderInvitationOverlay: function () {
var hasGuests = typeof this.state.participants === "object" && this.state.participants.filter(function (participant) {
return !participant.owner;
}).length > 0;
// Don't show if the room has participants whether from the room state or
// there being non-owner guests in the participants array.
return this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS && !hasGuests;
},
/**
* Works out if remote video should be rended or not, depending on the
* room state and other flags.
*
* @return {Boolean} True if remote video should be rended.
*
* XXX Refactor shouldRenderRemoteVideo & shouldRenderLoading into one fn
* that returns an enum
*/
shouldRenderRemoteVideo: function () {
switch (this.state.roomState) {
case ROOM_STATES.HAS_PARTICIPANTS:
if (this.state.remoteVideoEnabled) {
return true;
}
if (this.state.mediaConnected) {
// since the remoteVideo hasn't yet been enabled, if the
// media is connected, then we should be displaying an avatar.
return false;
}
return true;
case ROOM_STATES.READY:
case ROOM_STATES.GATHER:
case ROOM_STATES.INIT:
case ROOM_STATES.JOINING:
case ROOM_STATES.SESSION_CONNECTED:
case ROOM_STATES.JOINED:
case ROOM_STATES.MEDIA_WAIT:
// this case is so that we don't show an avatar while waiting for
// the other party to connect
return true;
case ROOM_STATES.CLOSING:
return true;
default:
console.warn("DesktopRoomConversationView.shouldRenderRemoteVideo:" + " unexpected roomState: ", this.state.roomState);
return true;
}
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a local
* stream is on its way from the camera?
*
* @returns {boolean}
* @private
*/
_isLocalLoading: function () {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT && !this.state.localSrcMediaElement;
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a remote
* stream is on its way from the other user?
*
* @returns {boolean}
* @private
*/
_isRemoteLoading: function () {
return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS && !this.state.remoteSrcMediaElement && !this.state.mediaConnected);
},
componentDidUpdate: function (prevProps, prevState) {
// Handle timestamp and window closing only when the call has terminated.
if (prevState.roomState === ROOM_STATES.ENDED && this.state.roomState === ROOM_STATES.ENDED) {
this.props.onCallTerminated();
}
},
handleContextMenu: function (e) {
e.preventDefault();
},
render: function () {
if (this.state.roomName || this.state.roomContextUrls) {
var roomTitle = this.state.roomName || this.state.roomContextUrls[0].description || this.state.roomContextUrls[0].location;
this.setTitle(roomTitle);
}
var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
var roomData = this.props.roomStore.getStoreState("activeRoom");
switch (this.state.roomState) {
case ROOM_STATES.FAILED:
case ROOM_STATES.FULL:
{
// Note: While rooms are set to hold a maximum of 2 participants, the
// FULL case should never happen on desktop.
return React.createElement(RoomFailureView, {
dispatcher: this.props.dispatcher,
failureReason: this.state.failureReason });
}
case ROOM_STATES.ENDED:
{
// When conversation ended we either display a feedback form or
// close the window. This is decided in the AppControllerView.
return null;
}
default:
{
return React.createElement(
"div",
{ className: "room-conversation-wrapper desktop-room-wrapper",
onContextMenu: this.handleContextMenu },
React.createElement(
sharedViews.MediaLayoutView,
{
dispatcher: this.props.dispatcher,
displayScreenShare: false,
isLocalLoading: this._isLocalLoading(),
isRemoteLoading: this._isRemoteLoading(),
isScreenShareLoading: false,
localPosterUrl: this.props.localPosterUrl,
localSrcMediaElement: this.state.localSrcMediaElement,
localVideoMuted: this.state.videoMuted,
matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
remotePosterUrl: this.props.remotePosterUrl,
remoteSrcMediaElement: this.state.remoteSrcMediaElement,
renderRemoteVideo: this.shouldRenderRemoteVideo(),
screenShareMediaElement: this.state.screenShareMediaElement,
screenSharePosterUrl: null,
showInitialContext: false,
useDesktopPaths: true },
React.createElement(sharedViews.ConversationToolbar, {
audio: { enabled: !this.state.audioMuted, visible: true },
dispatcher: this.props.dispatcher,
hangup: this.leaveRoom,
publishStream: this.publishStream,
showHangup: this.props.chatWindowDetached,
video: { enabled: !this.state.videoMuted, visible: true } }),
React.createElement(DesktopRoomInvitationView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
roomData: roomData,
show: shouldRenderInvitationOverlay,
socialShareProviders: this.state.socialShareProviders })
)
);
}
}
}
});
return {
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
FailureInfoView: FailureInfoView,
RoomFailureView: RoomFailureView,
SocialShareDropdown: SocialShareDropdown,
DesktopRoomConversationView: DesktopRoomConversationView,
DesktopRoomInvitationView: DesktopRoomInvitationView
};
}(document.mozL10n || navigator.mozL10n);

View File

@ -15,10 +15,10 @@
<div id="main"></div>
<script type="text/javascript" src="panels/vendor/l10n.js"></script>
<script type="text/javascript" src="shared/vendor/react-0.13.3.js"></script>
<script type="text/javascript" src="shared/vendor/lodash-3.9.3.js"></script>
<script type="text/javascript" src="shared/vendor/backbone-1.2.1.js"></script>
<script type="text/javascript" src="shared/vendor/classnames-2.2.0.js"></script>
<script type="text/javascript" src="shared/vendor/react.js"></script>
<script type="text/javascript" src="shared/vendor/lodash.js"></script>
<script type="text/javascript" src="shared/vendor/backbone.js"></script>
<script type="text/javascript" src="shared/vendor/classnames.js"></script>
<script type="text/javascript" src="shared/js/loopapi-client.js"></script>
<script type="text/javascript" src="shared/js/utils.js"></script>

View File

@ -7,4 +7,9 @@
"destructuring": true,
"forOf": true
},
"rules": {
// This is useful for some of the tests, e.g.
// expect(new Foo()).to.Throw(/error/)
"no-new": 0
}
}

View File

@ -5,13 +5,10 @@
describe("loop.conversation", function() {
"use strict";
var expect = chai.expect;
var FeedbackView = loop.feedbackViews.FeedbackView;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedModels = loop.shared.models;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var fakeWindow, sandbox, getLoopPrefStub, setLoopPrefStub, mozL10nGet;
var fakeWindow, sandbox, setLoopPrefStub, mozL10nGet;
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
@ -43,8 +40,6 @@ describe("loop.conversation", function() {
}
};
},
StartAlerting: sinon.stub(),
StopAlerting: sinon.stub(),
EnsureRegistered: sinon.stub(),
GetAppVersionInfo: function() {
return {
@ -53,7 +48,7 @@ describe("loop.conversation", function() {
platform: "test"
};
},
GetAudioBlob: sinon.spy(function(name) {
GetAudioBlob: sinon.spy(function() {
return new Blob([new ArrayBuffer(10)], { type: "audio/ogg" });
}),
GetSelectedTabMetadata: function() {

View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html>
<head>
<meta charset="utf-8">
<title>Loop desktop-local mocha tests</title>
<link rel="stylesheet" media="all" href="/test/vendor/mocha.css">
</head>
<body>
<div id="mocha">
<p><a href="../">Index</a></p>
</div>
<div id="messages"></div>
<div id="fixtures"></div>
<script src="/shared/vendor/lodash.js"></script>
<script src="/shared/test/loop_mocha_utils.js"></script>
<script>
LoopMochaUtils.trapErrors();
</script>
<!-- libs -->
<!-- test dependencies -->
<script src="/add-on/panels/vendor/l10n.js"></script>
<script src="/shared/vendor/react.js"></script>
<script src="/shared/vendor/classnames.js"></script>
<script src="/shared/vendor/backbone.js"></script>
<!-- test dependencies -->
<script src="/test/vendor/mocha.js"></script>
<script src="/test/vendor/chai.js"></script>
<script src="/test/vendor/chai-as-promised.js"></script>
<script src="/test/vendor/sinon.js"></script>
<script>
/*global chai,mocha */
chai.config.includeStack = true;
mocha.setup({ui: 'bdd', timeout: 10000});
</script>
<!-- App scripts -->
<script src="/add-on/shared/js/loopapi-client.js"></script>
<script src="/add-on/shared/js/utils.js"></script>
<script src="/add-on/shared/js/models.js"></script>
<script src="/add-on/shared/js/mixins.js"></script>
<script src="/add-on/shared/js/actions.js"></script>
<script src="/add-on/shared/js/validate.js"></script>
<script src="/add-on/shared/js/dispatcher.js"></script>
<script src="/add-on/shared/js/otSdkDriver.js"></script>
<script src="/add-on/shared/js/store.js"></script>
<script src="/add-on/shared/js/activeRoomStore.js"></script>
<script src="/add-on/shared/js/views.js"></script>
<script src="/add-on/shared/js/textChatStore.js"></script>
<script src="/add-on/shared/js/textChatView.js"></script>
<script src="/add-on/panels/js/conversationAppStore.js"></script>
<script src="/add-on/panels/js/roomStore.js"></script>
<script src="/add-on/panels/js/roomViews.js"></script>
<script src="/add-on/panels/js/feedbackViews.js"></script>
<script src="/add-on/panels/js/conversation.js"></script>
<script src="/add-on/panels/js/panel.js"></script>
<!-- Test scripts -->
<script src="conversationAppStore_test.js"></script>
<script src="conversation_test.js"></script>
<script src="feedbackViews_test.js"></script>
<script src="panel_test.js"></script>
<script src="roomViews_test.js"></script>
<script src="l10n_test.js"></script>
<script src="roomStore_test.js"></script>
<script>
// Stop the default init functions running to avoid conflicts in tests
document.removeEventListener('DOMContentLoaded', loop.panel.init);
document.removeEventListener('DOMContentLoaded', loop.conversation.init);
LoopMochaUtils.addErrorCheckingTests();
LoopMochaUtils.runTests();
</script>
</body>
</html>

View File

@ -8,13 +8,11 @@ describe("loop.panel", function() {
var expect = chai.expect;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sandbox, notifications, requestStubs;
var fakeXHR, fakeWindow, fakeEvent;
var requests = [];
var roomData, roomData2, roomList, roomName;
var mozL10nGetSpy;
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
@ -56,13 +54,16 @@ describe("loop.panel", function() {
GetPluralRule: sinon.stub(),
SetLoopPref: sinon.stub(),
GetLoopPref: function(prefName) {
if (prefName === "debug.dispatcher") {
return false;
}
return 1;
},
SetPanelHeight: function() { return null; },
GetPluralForm: function() {
return "fakeText";
},
"Rooms:GetAll": function(version) {
"Rooms:GetAll": function() {
return [];
},
"Rooms:PushSubscription": sinon.stub(),
@ -184,14 +185,9 @@ describe("loop.panel", function() {
});
describe("loop.panel.PanelView", function() {
var dispatcher, roomStore, callUrlData;
var dispatcher, roomStore;
beforeEach(function() {
callUrlData = {
callUrl: "http://call.invalid/",
expiresAt: 1000
};
dispatcher = new loop.Dispatcher();
roomStore = new loop.store.RoomStore(dispatcher, {
constants: {}
@ -216,7 +212,7 @@ describe("loop.panel", function() {
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.SettingsDropdown));
expect(view.getDOMNode().querySelectorAll(".icon-account"))
expect(view.getDOMNode().querySelectorAll(".entry-settings-account"))
.to.have.length.of(0);
});
@ -266,19 +262,11 @@ describe("loop.panel", function() {
expect(view.getDOMNode()).to.be.null;
});
it("should add ellipsis to text over 24chars", function() {
loop.storedRequests.GetUserProfile = { email: "reallyreallylongtext@example.com" };
var view = createTestPanelView();
var node = view.getDOMNode().querySelector(".user-identity");
expect(node.textContent).to.eql("reallyreallylongtext@exa…");
});
it("should warn when user profile is different from {} or null",
function() {
var warnstub = sandbox.stub(console, "warn");
var view = TestUtils.renderIntoDocument(React.createElement(
TestUtils.renderIntoDocument(React.createElement(
loop.panel.AccountLink, {
fxAEnabled: false,
userProfile: []
@ -294,7 +282,7 @@ describe("loop.panel", function() {
function() {
var warnstub = sandbox.stub(console, "warn");
var view = TestUtils.renderIntoDocument(React.createElement(
TestUtils.renderIntoDocument(React.createElement(
loop.panel.AccountLink, {
fxAEnabled: false,
userProfile: {}
@ -311,7 +299,7 @@ describe("loop.panel", function() {
React.createElement(loop.panel.SettingsDropdown));
}
var loginToFxAStub, logoutFromFxAStub, openFxASettingsStub;
var openFxASettingsStub;
beforeEach(function() {
openFxASettingsStub = sandbox.stub();
@ -342,7 +330,7 @@ describe("loop.panel", function() {
function() {
var view = mountTestComponent();
expect(view.getDOMNode().querySelectorAll(".icon-account"))
expect(view.getDOMNode().querySelectorAll(".entry-settings-account"))
.to.have.length.of(0);
});
@ -354,50 +342,80 @@ describe("loop.panel", function() {
sinon.assert.calledOnce(requestStubs.LoginToFxA);
});
});
it("should show a signout entry when user is authenticated", function() {
loop.storedRequests.GetUserProfile = { email: "test@example.com" };
it("should close the menu on clicking sign in", function() {
var view = mountTestComponent();
var view = mountTestComponent();
TestUtils.Simulate.click(view.getDOMNode()
.querySelector(".entry-settings-signin"));
sinon.assert.calledWithExactly(document.mozL10n.get,
"settings_menu_item_signout");
sinon.assert.neverCalledWith(document.mozL10n.get,
"settings_menu_item_signin");
});
it("should show an account entry when user is authenticated", function() {
LoopMochaUtils.stubLoopRequest({
GetUserProfile: function() { return { email: "test@example.com" }; }
expect(view.state.showMenu).eql(false);
});
var view = mountTestComponent();
it("should close the panel on clicking sign in", function() {
var view = mountTestComponent();
sinon.assert.calledWithExactly(document.mozL10n.get,
"settings_menu_item_settings");
TestUtils.Simulate.click(view.getDOMNode()
.querySelector(".entry-settings-signin"));
sinon.assert.calledOnce(fakeWindow.close);
});
});
it("should open the FxA settings when the account entry is clicked",
function() {
loop.storedRequests.GetUserProfile = { email: "test@example.com" };
describe("UserLoggedIn", function() {
var view;
var view = mountTestComponent();
beforeEach(function() {
loop.storedRequests.GetUserProfile = { email: "test@example.com" };
view = mountTestComponent();
});
TestUtils.Simulate.click(view.getDOMNode()
.querySelector(".entry-settings-account"));
it("should show a signout entry when user is authenticated", function() {
expect(view.getDOMNode().querySelectorAll(".entry-settings-signout"))
.to.have.length.of(1);
expect(view.getDOMNode().querySelectorAll(".entry-settings-signin"))
.to.have.length.of(0);
});
sinon.assert.calledOnce(openFxASettingsStub);
});
it("should show an account entry when user is authenticated", function() {
expect(view.getDOMNode().querySelectorAll(".entry-settings-account"))
.to.have.length.of(1);
});
it("should sign out the user on click when authenticated", function() {
loop.storedRequests.GetUserProfile = { email: "test@example.com" };
var view = mountTestComponent();
it("should open the FxA settings when the account entry is clicked",
function() {
TestUtils.Simulate.click(view.getDOMNode()
.querySelector(".entry-settings-account"));
TestUtils.Simulate.click(view.getDOMNode()
.querySelector(".entry-settings-signout"));
sinon.assert.calledOnce(openFxASettingsStub);
});
sinon.assert.calledOnce(requestStubs.LogoutFromFxA);
it("should sign out the user on click when authenticated", function() {
TestUtils.Simulate.click(view.getDOMNode()
.querySelector(".entry-settings-signout"));
sinon.assert.calledOnce(requestStubs.LogoutFromFxA);
});
it("should close the dropdown menu on clicking sign out", function() {
LoopMochaUtils.stubLoopRequest({
GetUserProfile: function() { return { email: "test@example.com" }; }
});
view.setState({ showMenu: true });
TestUtils.Simulate.click(view.getDOMNode()
.querySelector(".entry-settings-signout"));
expect(view.state.showMenu).eql(false);
});
it("should not close the panel on clicking sign out", function() {
TestUtils.Simulate.click(view.getDOMNode()
.querySelector(".entry-settings-signout"));
sinon.assert.notCalled(fakeWindow.close);
});
});
describe("Toggle Notifications", function() {
@ -813,22 +831,6 @@ describe("loop.panel", function() {
});
});
describe("Room Entry click", function() {
var roomEntry, roomEntryNode;
beforeEach(function() {
sandbox.stub(dispatcher, "dispatch");
roomEntry = mountRoomEntry({
dispatcher: dispatcher,
isOpenedRoom: false,
room: new loop.store.Room(roomData)
});
roomEntryNode = roomEntry.getDOMNode();
});
});
describe("Room name updated", function() {
it("should update room name", function() {
var roomEntry = mountRoomEntry({
@ -953,7 +955,7 @@ describe("loop.panel", function() {
});
it("should close the panel once a room is created and there is no error", function() {
var view = createTestComponent();
createTestComponent();
roomStore.setStoreState({ pendingCreation: true });
@ -964,20 +966,10 @@ describe("loop.panel", function() {
sinon.assert.calledOnce(fakeWindow.close);
});
it("should render the no rooms view when no rooms available", function() {
it("should not render the room list view when no rooms available", function() {
var view = createTestComponent();
var node = view.getDOMNode();
expect(node.querySelectorAll(".room-list-empty").length).to.eql(1);
});
it("should call mozL10n.get for room empty strings", function() {
var view = createTestComponent();
sinon.assert.calledWithExactly(document.mozL10n.get,
"no_conversations_message_heading2");
sinon.assert.calledWithExactly(document.mozL10n.get,
"no_conversations_start_message2");
expect(node.querySelectorAll(".room-list").length).to.eql(0);
});
it("should display a loading animation when rooms are pending", function() {

View File

@ -134,7 +134,7 @@ describe("loop.store.RoomStore", function() {
describe("MozLoop rooms event listeners", function() {
beforeEach(function() {
LoopMochaUtils.stubLoopRequest({
"Rooms:GetAll": function(version) {
"Rooms:GetAll": function() {
return fakeRoomList;
}
});
@ -226,8 +226,6 @@ describe("loop.store.RoomStore", function() {
});
describe("#createRoom", function() {
var fakeLocalRoomId = "777";
var fakeOwner = "fake@invalid";
var fakeRoomCreationData;
beforeEach(function() {
@ -320,6 +318,19 @@ describe("loop.store.RoomStore", function() {
}));
});
it("should dispatch a CreateRoomError action if the operation fails with no result",
function() {
requestStubs["Rooms:Create"].returns();
store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.CreateRoomError({
error: new Error("no result")
}));
});
it("should log a telemetry event when the operation is successful", function() {
store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
@ -339,6 +350,16 @@ describe("loop.store.RoomStore", function() {
sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue,
"LOOP_ROOM_CREATE", 1);
});
it("should log a telemetry event when the operation fails with no result", function() {
requestStubs["Rooms:Create"].returns();
store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
sinon.assert.calledOnce(requestStubs.TelemetryAddValue);
sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue,
"LOOP_ROOM_CREATE", 1);
});
});
describe("#createdRoom", function() {
@ -725,14 +746,14 @@ describe("loop.store.RoomStore", function() {
});
describe("#openRoom", function() {
var store, fakeMozLoop;
var store;
beforeEach(function() {
store = new loop.store.RoomStore(dispatcher, { constants: {} });
});
it("should open the room via mozLoop", function() {
dispatcher.dispatch(new sharedActions.OpenRoom({ roomToken: "42abc" }));
store.openRoom(new sharedActions.OpenRoom({ roomToken: "42abc" }));
sinon.assert.calledOnce(requestStubs["Rooms:Open"]);
sinon.assert.calledWithExactly(requestStubs["Rooms:Open"], "42abc");
@ -781,7 +802,7 @@ describe("loop.store.RoomStore", function() {
expect(store.getStoreState().savingContext).to.eql(false);
LoopMochaUtils.stubLoopRequest({
"Rooms:Update": function(roomToken, roomData) {
"Rooms:Update": function() {
expect(store.getStoreState().savingContext).to.eql(true);
}
});
@ -799,7 +820,7 @@ describe("loop.store.RoomStore", function() {
err.isError = true;
LoopMochaUtils.stubLoopRequest({
"Rooms:Update": function(roomToken, roomData) {
"Rooms:Update": function() {
expect(store.getStoreState().savingContext).to.eql(true);
return err;
}

View File

@ -7,21 +7,19 @@ describe("loop.roomViews", function() {
var expect = chai.expect;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var ROOM_STATES = loop.store.ROOM_STATES;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var sandbox, dispatcher, roomStore, activeRoomStore, view;
var clock, fakeWindow, requestStubs, fakeContextURL;
var clock, fakeWindow, requestStubs;
var favicon = "data:image/x-icon;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
LoopMochaUtils.stubLoopRequest(requestStubs = {
GetAudioBlob: sinon.spy(function(name) {
GetAudioBlob: sinon.spy(function() {
return new Blob([new ArrayBuffer(10)], { type: "audio/ogg" });
}),
GetLoopPref: sinon.stub(),
@ -79,11 +77,6 @@ describe("loop.roomViews", function() {
textChatStore: textChatStore
});
fakeContextURL = {
description: "An invalid page",
location: "http://invalid.com",
thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
};
sandbox.stub(dispatcher, "dispatch");
});
@ -315,16 +308,6 @@ describe("loop.roomViews", function() {
expect(copyBtn.textContent).eql("invite_copy_link_button");
});
});
describe("Edit Context", function() {
it("should show the edit context view", function() {
view = mountTestComponent({
showEditContext: true
});
expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
});
});
});
describe("DesktopRoomConversationView", function() {
@ -433,7 +416,7 @@ describe("loop.roomViews", function() {
});
describe("#componentWillUpdate", function() {
function expectActionDispatched(component) {
function expectActionDispatched() {
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
sinon.match.instanceOf(sharedActions.SetupStreamElements));
@ -441,29 +424,29 @@ describe("loop.roomViews", function() {
it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state is entered", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
var component = mountTestComponent();
mountTestComponent();
activeRoomStore.setStoreState({ roomState: ROOM_STATES.MEDIA_WAIT });
expectActionDispatched(component);
expectActionDispatched();
});
it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is re-entered", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.ENDED });
var component = mountTestComponent();
mountTestComponent();
activeRoomStore.setStoreState({ roomState: ROOM_STATES.MEDIA_WAIT });
expectActionDispatched(component);
expectActionDispatched();
});
it("should dispatch a `StartBrowserShare` action when the SESSION_CONNECTED state is entered", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
var component = mountTestComponent();
mountTestComponent();
activeRoomStore.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED });
expectActionDispatched("startBrowserShare");
expectActionDispatched();
});
});
@ -680,7 +663,6 @@ describe("loop.roomViews", function() {
});
describe("Room name priority", function() {
var roomEntry;
beforeEach(function() {
activeRoomStore.setStoreState({
participants: [{}],
@ -721,29 +703,6 @@ describe("loop.roomViews", function() {
});
});
});
describe("Edit Context", function() {
it("should show the form when the edit button is clicked", function() {
view = mountTestComponent();
var node = view.getDOMNode();
expect(node.querySelector(".room-context")).to.eql(null);
var editButton = node.querySelector(".settings-menu > li.entry-settings-edit");
React.addons.TestUtils.Simulate.click(editButton);
expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
});
it("should not have a settings menu when the edit button is clicked", function() {
view = mountTestComponent();
var editButton = view.getDOMNode().querySelector(".settings-menu > li.entry-settings-edit");
React.addons.TestUtils.Simulate.click(editButton);
expect(view.getDOMNode().querySelector(".settings-menu")).to.eql(null);
});
});
});
describe("SocialShareDropdown", function() {
@ -839,175 +798,4 @@ describe("loop.roomViews", function() {
});
});
});
describe("DesktopRoomEditContextView", function() {
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
savingContext: false,
show: true,
roomData: {
roomToken: "fakeToken"
}
}, props);
return TestUtils.renderIntoDocument(
React.createElement(loop.roomViews.DesktopRoomEditContextView, props));
}
describe("#render", function() {
it("should not render the component when 'show' is false", function() {
view = mountTestComponent({
show: false
});
expect(view.getDOMNode()).to.eql(null);
});
it("should close the view when the cancel button is clicked", function() {
view = mountTestComponent({
roomData: { roomContextUrls: [fakeContextURL] }
});
var closeBtn = view.getDOMNode().querySelector(".button-cancel");
React.addons.TestUtils.Simulate.click(closeBtn);
expect(view.getDOMNode()).to.eql(null);
});
it("should render the view correctly", function() {
var roomName = "Hello, is it me you're looking for?";
view = mountTestComponent({
roomData: {
roomName: roomName,
roomContextUrls: [fakeContextURL]
}
});
var node = view.getDOMNode();
expect(node.querySelector("form")).to.not.eql(null);
// Check the contents of the form fields.
expect(node.querySelector(".room-context-name").value).to.eql(roomName);
expect(node.querySelector(".room-context-url").value).to.eql(fakeContextURL.location);
expect(node.querySelector(".room-context-comments").value).to.eql(fakeContextURL.description);
});
});
describe("Update Room", function() {
var roomNameBox;
beforeEach(function() {
view = mountTestComponent({
editMode: true,
roomData: {
roomToken: "fakeToken",
roomName: "fakeName",
roomContextUrls: [fakeContextURL]
}
});
roomNameBox = view.getDOMNode().querySelector(".room-context-name");
});
it("should dispatch a UpdateRoomContext action when the save button is clicked",
function() {
React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
value: "reallyFake"
} });
React.addons.TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-accept"));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomContext({
roomToken: "fakeToken",
newRoomName: "reallyFake",
newRoomDescription: fakeContextURL.description,
newRoomURL: fakeContextURL.location,
newRoomThumbnail: fakeContextURL.thumbnail
}));
});
it("should dispatch a UpdateRoomContext action when Enter key is pressed",
function() {
React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
value: "reallyFake"
} });
TestUtils.Simulate.keyDown(roomNameBox, { key: "Enter", which: 13 });
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomContext({
roomToken: "fakeToken",
newRoomName: "reallyFake",
newRoomDescription: fakeContextURL.description,
newRoomURL: fakeContextURL.location,
newRoomThumbnail: fakeContextURL.thumbnail
}));
});
it("should close the edit form when context was saved successfully", function(done) {
view.setProps({ savingContext: true }, function() {
var node = view.getDOMNode();
// The button should show up as disabled.
expect(node.querySelector(".button-accept").hasAttribute("disabled")).to.eql(true);
// Now simulate a successful save.
view.setProps({ savingContext: false }, function() {
// The 'show flag should be updated.
expect(view.state.show).to.eql(false);
done();
});
});
});
});
describe("#handleContextClick", function() {
var fakeEvent;
beforeEach(function() {
fakeEvent = {
preventDefault: sinon.stub(),
stopPropagation: sinon.stub()
};
});
it("should not attempt to open a URL when none is attached", function() {
view = mountTestComponent({
roomData: {
roomToken: "fakeToken",
roomName: "fakeName"
}
});
view.handleContextClick(fakeEvent);
sinon.assert.calledOnce(fakeEvent.preventDefault);
sinon.assert.calledOnce(fakeEvent.stopPropagation);
sinon.assert.notCalled(requestStubs.OpenURL);
sinon.assert.notCalled(requestStubs.TelemetryAddValue);
});
it("should open a URL", function() {
view = mountTestComponent({
roomData: {
roomToken: "fakeToken",
roomName: "fakeName",
roomContextUrls: [fakeContextURL]
}
});
view.handleContextClick(fakeEvent);
sinon.assert.calledOnce(fakeEvent.preventDefault);
sinon.assert.calledOnce(fakeEvent.stopPropagation);
sinon.assert.calledOnce(requestStubs.OpenURL);
sinon.assert.calledWithExactly(requestStubs.OpenURL, fakeContextURL.location);
sinon.assert.calledOnce(requestStubs.TelemetryAddValue);
sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue,
"LOOP_ROOM_CONTEXT_CLICK", 1);
});
});
});
});

View File

@ -1,7 +1,7 @@
# need to get this dir in the path so that we make the import work
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'shared'))
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'shared', 'test'))
from frontend_tester import BaseTestFrontendUnits
@ -10,7 +10,7 @@ class TestDesktopUnits(BaseTestFrontendUnits):
def setUp(self):
super(TestDesktopUnits, self).setUp()
self.set_server_prefix("../desktop-local/")
self.set_server_prefix("../../../../")
def test_units(self):
self.check_page("index.html")
self.check_page("chrome/content/panels/test/index.html")

View File

@ -221,14 +221,19 @@
get: translateString,
// get the document language
getLanguage: function() { return gLanguage; },
// get the direction (ltr|rtl) of the current language
getDirection: function() {
// http://www.w3.org/International/questions/qa-scripts
// Arabic, Hebrew, Farsi, Pashto, Urdu
var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr';
language: {
set code(lang) {
throw new Error("unsupported");
},
get code() {
return gLanguage;
},
get direction() {
// http://www.w3.org/International/questions/qa-scripts
// Arabic, Hebrew, Farsi, Pashto, Urdu
var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr';
}
},
// translate an element or document fragment

View File

@ -2,7 +2,7 @@ pref("loop.enabled", true);
pref("loop.textChat.enabled", true);
pref("loop.server", "https://loop.services.mozilla.com/v0");
pref("loop.linkClicker.url", "https://hello.firefox.com/");
pref("loop.gettingStarted.latestFTUVersion", 0);
pref("loop.gettingStarted.latestFTUVersion", 1);
pref("loop.facebook.shareUrl", "https://www.facebook.com/sharer/sharer.php?u=%ROOM_URL%");
pref("loop.gettingStarted.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/hello/start/");
pref("loop.gettingStarted.resumeOnFirstJoin", false);
@ -10,7 +10,6 @@ pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
pref("loop.legal.ToS_url", "https://www.mozilla.org/about/legal/terms/firefox-hello/");
pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/firefox-hello/");
pref("loop.do_not_disturb", false);
pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/ringtone.ogg");
pref("loop.retry_delay.start", 60000);
pref("loop.retry_delay.limit", 300000);
pref("loop.ping.interval", 1800000);
@ -30,5 +29,5 @@ pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src * data:; font
#endif
pref("loop.fxa_oauth.tokendata", "");
pref("loop.fxa_oauth.profile", "");
pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");
pref("loop.support_url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/cobrowsing");
pref("loop.browserSharing.showInfoBar", true);

View File

@ -587,10 +587,6 @@ html[dir="rtl"] .context-wrapper > .context-preview {
clear: both;
}
.clicks-allowed.context-wrapper:hover {
border: 2px solid #5cccee;
}
/* Only underline the url, not the associated text */
.clicks-allowed.context-wrapper:hover > .context-info > .context-url {
text-decoration: underline;

View File

@ -15,10 +15,10 @@ button::-moz-focus-inner {
z-index: 1020; /* required to have it superimposed to the video element */
border: 0;
left: 1.2rem;
right: 1.2rem;
height: 2.4rem;
position: absolute;
bottom: 1.2rem;
text-align: center;
}
html[dir="rtl"] .conversation-toolbar {
@ -95,14 +95,6 @@ html[dir="rtl"] .conversation-toolbar-media-btn-group-box > button:first-child:h
background-size: cover;
}
.conversation-toolbar-btn-box.btn-edit-entry {
float: right;
}
html[dir="rtl"] .conversation-toolbar-btn-box.btn-edit-entry {
float: left;
}
/* conversationViews.jsx */
.conversation-toolbar .btn-hangup {
@ -168,8 +160,7 @@ html[dir="rtl"] .conversation-toolbar-btn-box.btn-edit-entry {
width: 100%;
}
.call-action-group > .btn,
.room-context > .btn {
.call-action-group > .btn {
min-height: 30px;
border-radius: 4px;
margin: 0 4px;
@ -509,92 +500,15 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
margin: 0 1rem 1.5rem 1rem;
}
.room-context {
background: #fff;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
font-size: .9em;
display: flex;
flex-flow: column nowrap;
align-content: flex-start;
align-items: flex-start;
overflow-x: hidden;
overflow-y: auto;
/* Make the context view float atop the video elements. */
z-index: 3;
}
.room-invitation-overlay .room-context {
position: relative;
left: auto;
bottom: auto;
flex: 0 1 auto;
height: 100%;
}
.room-context > .error-display-area.error {
display: block;
background-color: rgba(215,67,69,.8);
border-radius: 3px;
padding: .5em;
}
.room-context > .error-display-area {
display: none;
}
.room-context > .error-display-area.error {
margin: 1em 0 .5em 0;
text-align: center;
text-shadow: 1px 1px 0 rgba(0,0,0,.3);
}
.room-invitation-content,
.room-context-header {
.room-invitation-content {
color: #333;
font-size: 1rem;
font-weight: bold;
margin: 1rem auto;
}
.room-context > form {
margin-bottom: 1rem;
padding: .5rem;
width: 100%;
}
.room-context > form > textarea,
.room-context > form > input[type="text"] {
border: 1px solid #c3c3c3;
border-radius: 4px;
box-shadow: none;
color: #4a4a4a;
display: block;
font-size: 1.1rem;
height: 2.6rem;
margin: 10px 0;
outline: none;
padding: 6px;
width: 100%;
}
.room-context > form > textarea {
font-family: inherit;
height: 5.2rem;
resize: none;
}
.room-context > form > textarea::-moz-placeholder,
.room-context > form > input::-moz-placeholder {
color: #999;
}
.room-context > form > textarea:focus,
.room-context > form > input:focus {
border: 0.1rem solid #5cccee;
.room-invitation-content > p {
margin-left: 10px;
margin-right: 10px;
}
.media-layout {
@ -802,8 +716,16 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
}
}
/* e.g. very narrow widths similar to conversation window */
@media screen and (max-width:350px) {
/* e.g. very narrow widths similar to conversation window.
Note: on some displays (e.g. windows / medium size) the width
may be very slightly over the expected width, so we add on 2px
just in case. */
@media screen and (max-width:352px) {
.conversation-toolbar {
left: 50%;
transform: translateX(-50%);
}
.media-layout > .media-wrapper {
flex-flow: column nowrap;
}
@ -905,23 +827,23 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
/* aligns paragraph to right side */
justify-content: flex-end;
margin-left: 0;
margin-right: 4px;
margin-right: 10px;
}
.text-chat-entry.received {
margin-left: 2px;
margin-left: 10px;
margin-right: 0;
}
html[dir="rtl"] .text-chat-entry.sent {
margin-left: 5px;
margin-left: 10px;
margin-right: 0;
}
html[dir="rtl"] .text-chat-entry.received {
margin-left: 0;
margin-right: 5px;
margin-right: 10px;
}
.text-chat-entry > p {
@ -940,6 +862,11 @@ html[dir="rtl"] .text-chat-entry.received {
order: 1;
}
.text-chat-entry > .context-wrapper {
flex: 0 1 auto;
order: 1;
}
.text-chat-entry.sent > p {
border-bottom-right-radius: 0;
}
@ -1056,6 +983,21 @@ html[dir="rtl"] .text-chat-entry.received .text-chat-arrow {
margin-left: -10px;
}
/* Context updated */
.text-chat-entry > .context-content > .context-wrapper {
max-width: 300px;
min-height: 50px;
background: #fff;
border: 1px solid rgba(0,0,0,.10);
border-radius: 4px;
padding: 10px;
box-shadow: 0 1px 1px rgba(0,0,0,.15);
}
.text-chat-entry.received > .context-content > .context-wrapper {
max-width: 180px;
}
.text-chat-header.special.room-name {
color: #666;
font-weight: bold;
@ -1120,8 +1062,11 @@ html[dir="rtl"] .text-chat-entry.received .text-chat-arrow {
border-top: 1px solid #66c9f2;
}
/* e.g. very narrow widths similar to conversation window */
@media screen and (max-width:350px) {
/* e.g. very narrow widths similar to conversation window.
Note: on some displays (e.g. windows / medium size) the width
may be very slightly over the expected width, so we add on 2px
just in case. */
@media screen and (max-width:352px) {
.text-chat-view {
display: flex;
flex-flow: column nowrap;

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 532 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 535 B

After

Width:  |  Height:  |  Size: 535 B

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 225 B

After

Width:  |  Height:  |  Size: 225 B

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 218 B

After

Width:  |  Height:  |  Size: 218 B

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 451 B

View File

Before

Width:  |  Height:  |  Size: 832 B

After

Width:  |  Height:  |  Size: 832 B

View File

Before

Width:  |  Height:  |  Size: 556 B

After

Width:  |  Height:  |  Size: 556 B

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 599 B

After

Width:  |  Height:  |  Size: 599 B

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