Merge mozilla-central to b2g-inbound
|
@ -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
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
#include "nsITreeBoxObject.h"
|
||||
#include "nsITreeColumns.h"
|
||||
#include "mozilla/dom/Element.h"
|
||||
#include "mozilla/dom/HTMLLabelElement.h"
|
||||
|
||||
using namespace mozilla;
|
||||
|
||||
|
@ -41,6 +42,16 @@ using namespace mozilla;
|
|||
// nsCoreUtils
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool
|
||||
nsCoreUtils::IsLabelWithControl(nsIContent* aContent)
|
||||
{
|
||||
dom::HTMLLabelElement* label = dom::HTMLLabelElement::FromContent(aContent);
|
||||
if (label && label->GetControl())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
nsCoreUtils::HasClickListener(nsIContent *aContent)
|
||||
{
|
||||
|
|
|
@ -28,6 +28,11 @@ class nsIWidget;
|
|||
class nsCoreUtils
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Return true if the given node is a label of a control.
|
||||
*/
|
||||
static bool IsLabelWithControl(nsIContent *aContent);
|
||||
|
||||
/**
|
||||
* Return true if the given node has registered click, mousedown or mouseup
|
||||
* event listeners.
|
||||
|
|
|
@ -115,13 +115,14 @@ LinkableAccessible::Value(nsString& aValue)
|
|||
uint8_t
|
||||
LinkableAccessible::ActionCount()
|
||||
{
|
||||
bool isLink, isOnclick;
|
||||
ActionWalk(&isLink, &isOnclick);
|
||||
return (isLink || isOnclick) ? 1 : 0;
|
||||
bool isLink, isOnclick, isLabelWithControl;
|
||||
ActionWalk(&isLink, &isOnclick, &isLabelWithControl);
|
||||
return (isLink || isOnclick || isLabelWithControl) ? 1 : 0;
|
||||
}
|
||||
|
||||
Accessible*
|
||||
LinkableAccessible::ActionWalk(bool* aIsLink, bool* aIsOnclick)
|
||||
LinkableAccessible::ActionWalk(bool* aIsLink, bool* aIsOnclick,
|
||||
bool* aIsLabelWithControl)
|
||||
{
|
||||
if (aIsOnclick) {
|
||||
*aIsOnclick = false;
|
||||
|
@ -129,6 +130,9 @@ LinkableAccessible::ActionWalk(bool* aIsLink, bool* aIsOnclick)
|
|||
if (aIsLink) {
|
||||
*aIsLink = false;
|
||||
}
|
||||
if (aIsLabelWithControl) {
|
||||
*aIsLabelWithControl = false;
|
||||
}
|
||||
|
||||
if (nsCoreUtils::HasClickListener(mContent)) {
|
||||
if (aIsOnclick) {
|
||||
|
@ -155,6 +159,13 @@ LinkableAccessible::ActionWalk(bool* aIsLink, bool* aIsOnclick)
|
|||
}
|
||||
return walkUpAcc;
|
||||
}
|
||||
|
||||
if (nsCoreUtils::IsLabelWithControl(walkUpAcc->GetContent())) {
|
||||
if (aIsLabelWithControl) {
|
||||
*aIsLabelWithControl = true;
|
||||
}
|
||||
return walkUpAcc;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
@ -166,11 +177,11 @@ LinkableAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName)
|
|||
|
||||
// Action 0 (default action): Jump to link
|
||||
if (aIndex == eAction_Jump) {
|
||||
bool isOnclick, isLink;
|
||||
ActionWalk(&isLink, &isOnclick);
|
||||
bool isOnclick, isLink, isLabelWithControl;
|
||||
ActionWalk(&isLink, &isOnclick, &isLabelWithControl);
|
||||
if (isLink) {
|
||||
aName.AssignLiteral("jump");
|
||||
} else if (isOnclick) {
|
||||
} else if (isOnclick || isLabelWithControl) {
|
||||
aName.AssignLiteral("click");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,7 +76,8 @@ public:
|
|||
|
||||
// ActionAccessible helpers
|
||||
Accessible* ActionWalk(bool* aIsLink = nullptr,
|
||||
bool* aIsOnclick = nullptr);
|
||||
bool* aIsOnclick = nullptr,
|
||||
bool* aIsLabelWithControl = nullptr);
|
||||
// HyperLinkAccessible
|
||||
virtual already_AddRefed<nsIURI> AnchorURIAt(uint32_t aAnchorIndex) override;
|
||||
|
||||
|
|
|
@ -75,6 +75,32 @@ HTMLLabelAccessible::RelationByType(RelationType aType)
|
|||
return rel;
|
||||
}
|
||||
|
||||
uint8_t
|
||||
HTMLLabelAccessible::ActionCount()
|
||||
{
|
||||
return nsCoreUtils::IsLabelWithControl(mContent) ? 1 : 0;
|
||||
}
|
||||
|
||||
void
|
||||
HTMLLabelAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName)
|
||||
{
|
||||
if (aIndex == 0) {
|
||||
if (nsCoreUtils::IsLabelWithControl(mContent))
|
||||
aName.AssignLiteral("click");
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
HTMLLabelAccessible::DoAction(uint8_t aIndex)
|
||||
{
|
||||
if (aIndex != 0)
|
||||
return false;
|
||||
|
||||
DoCommand();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// nsHTMLOuputAccessible
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -62,6 +62,11 @@ public:
|
|||
// Accessible
|
||||
virtual Relation RelationByType(RelationType aType) override;
|
||||
|
||||
// ActionAccessible
|
||||
virtual uint8_t ActionCount() override;
|
||||
virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
|
||||
virtual bool DoAction(uint8_t aIndex) override;
|
||||
|
||||
protected:
|
||||
virtual ~HTMLLabelAccessible() {}
|
||||
virtual ENameValueFlag NativeName(nsString& aName) override;
|
||||
|
|
|
@ -39,11 +39,19 @@
|
|||
ID: "onclick_img",
|
||||
actionName: "click",
|
||||
events: CLICK_EVENTS
|
||||
},
|
||||
{
|
||||
ID: "label1",
|
||||
actionName: "click",
|
||||
events: CLICK_EVENTS
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
testActions(actionsArray);
|
||||
|
||||
is(getAccessible("label1").firstChild.actionCount, 1, "label text should have 1 action");
|
||||
|
||||
getAccessible("onclick_img").takeFocus();
|
||||
is(getAccessible("link1").actionCount, 1, "links should have one action");
|
||||
is(getAccessible("link2").actionCount, 1, "link with onclick handler should have 1 action");
|
||||
|
@ -87,5 +95,13 @@
|
|||
|
||||
<a id="link1" href="www">linkable textleaf accessible</a>
|
||||
<div id="link2" onclick="">linkable textleaf accessible</div>
|
||||
|
||||
<div>
|
||||
<label for="TextBox_t2" id="label1">
|
||||
<span>Explicit</span>
|
||||
</label>
|
||||
<input name="in2" id="TextBox_t2" type="text" maxlength="17">
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -60,6 +60,11 @@
|
|||
actionName: "press",
|
||||
events: CLICK_EVENTS
|
||||
},
|
||||
{
|
||||
ID: "name_entry_label",
|
||||
actionName: "click",
|
||||
events: CLICK_EVENTS
|
||||
},
|
||||
{
|
||||
ID: "labelWithPopup",
|
||||
actionName: "click",
|
||||
|
@ -72,6 +77,8 @@
|
|||
}*/
|
||||
];
|
||||
|
||||
is(getAccessible("name_entry_label").firstChild.actionCount, 1, "label text should have 1 action");
|
||||
|
||||
testActions(actionsArray);
|
||||
}
|
||||
|
||||
|
@ -125,6 +132,10 @@
|
|||
<label id="labelWithPopup" value="file name"
|
||||
popup="fileContext"
|
||||
tabindex="0"/>
|
||||
<hbox>
|
||||
<label id="name_entry_label" value="Name" control="name_entry"/>
|
||||
<textbox id="name_entry"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</hbox>
|
||||
</window>
|
||||
|
|
|
@ -252,7 +252,9 @@ ReserveFileDescriptors(FdArray& aReservedFds)
|
|||
MOZ_CRASH("ProcLoader error: failed to reserve a magic file descriptor.");
|
||||
}
|
||||
|
||||
aReservedFds.append(target);
|
||||
if (!aReservedFds.append(target)) {
|
||||
MOZ_CRASH("Failed to append to aReservedFds");
|
||||
}
|
||||
|
||||
if (fd == target) {
|
||||
// No need to call dup2(). We already occupy the desired file descriptor.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -10,6 +10,7 @@ var Cc = Components.classes;
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/NotificationDB.jsm");
|
||||
Cu.import("resource:///modules/RecentWindow.jsm");
|
||||
Cu.import("resource:///modules/UserContextUI.jsm");
|
||||
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
|
||||
|
@ -4059,24 +4060,7 @@ function updateUserContextUIIndicator(browser)
|
|||
let label = document.getElementById("userContext-label");
|
||||
let userContextId = browser.getAttribute("usercontextid");
|
||||
hbox.setAttribute("usercontextid", userContextId);
|
||||
switch (userContextId) {
|
||||
case "1":
|
||||
label.value = gBrowserBundle.GetStringFromName("usercontext.personal.label");
|
||||
break;
|
||||
case "2":
|
||||
label.value = gBrowserBundle.GetStringFromName("usercontext.work.label");
|
||||
break;
|
||||
case "3":
|
||||
label.value = gBrowserBundle.GetStringFromName("usercontext.banking.label");
|
||||
break;
|
||||
case "4":
|
||||
label.value = gBrowserBundle.GetStringFromName("usercontext.shopping.label");
|
||||
break;
|
||||
// Display the context IDs for values outside the pre-defined range.
|
||||
// Used for debugging, no localization necessary.
|
||||
default:
|
||||
label.value = "Context " + userContextId;
|
||||
}
|
||||
label.value = UserContextUI.getUserContextLabel(userContextId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
@ -758,7 +758,7 @@
|
|||
if (this.mBrowser.userTypedClear > 0 ||
|
||||
((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
|
||||
aLocation.spec != "about:blank") ||
|
||||
aFlags && Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
|
||||
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
|
||||
this.mBrowser.userTypedValue = null;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -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]
|
||||
|
@ -417,7 +417,7 @@ skip-if = os == "linux" || os == "mac" # No tabs in titlebar on linux
|
|||
# Disabled on OS X because of bug 967917
|
||||
[browser_tabfocus.js]
|
||||
[browser_tabkeynavigation.js]
|
||||
skip-if = e10s
|
||||
skip-if = (os == "mac" && !e10s)
|
||||
[browser_tabopen_reflows.js]
|
||||
[browser_tabs_close_beforeunload.js]
|
||||
support-files =
|
||||
|
|
|
@ -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");
|
||||
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");
|
||||
}
|
||||
gObservedTopics = {};
|
||||
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");
|
||||
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);
|
||||
|
|
|
@ -42,23 +42,19 @@ function test() {
|
|||
|
||||
is(gBrowser.selectedTab, tab1,
|
||||
"Tab1 should be activated");
|
||||
EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true },
|
||||
browser1.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true });
|
||||
is(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be activated by pressing Ctrl+Tab on Tab1");
|
||||
|
||||
EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true },
|
||||
browser2.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true });
|
||||
is(gBrowser.selectedTab, tab3,
|
||||
"Tab3 should be activated by pressing Ctrl+Tab on Tab2");
|
||||
|
||||
EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: true },
|
||||
browser3.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: true });
|
||||
is(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be activated by pressing Ctrl+Shift+Tab on Tab3");
|
||||
|
||||
EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: true },
|
||||
browser2.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: true });
|
||||
is(gBrowser.selectedTab, tab1,
|
||||
"Tab1 should be activated by pressing Ctrl+Shift+Tab on Tab2");
|
||||
|
||||
|
@ -67,23 +63,19 @@ function test() {
|
|||
|
||||
is(gBrowser.selectedTab, tab1,
|
||||
"Tab1 should be activated");
|
||||
EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true },
|
||||
browser1.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true });
|
||||
is(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be activated by pressing Ctrl+PageDown on Tab1");
|
||||
|
||||
EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true },
|
||||
browser2.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true });
|
||||
is(gBrowser.selectedTab, tab3,
|
||||
"Tab3 should be activated by pressing Ctrl+PageDown on Tab2");
|
||||
|
||||
EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true },
|
||||
browser3.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true });
|
||||
is(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be activated by pressing Ctrl+PageUp on Tab3");
|
||||
|
||||
EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true },
|
||||
browser2.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true });
|
||||
is(gBrowser.selectedTab, tab1,
|
||||
"Tab1 should be activated by pressing Ctrl+PageUp on Tab2");
|
||||
|
||||
|
@ -98,23 +90,19 @@ function test() {
|
|||
|
||||
is(gBrowser.selectedTab, tab1,
|
||||
"Tab1 should be activated");
|
||||
EventUtils.synthesizeKey(advanceKey, { altKey: true, metaKey: true },
|
||||
browser1.contentWindow);
|
||||
EventUtils.synthesizeKey(advanceKey, { altKey: true, metaKey: true });
|
||||
is(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be activated by pressing Ctrl+" + advanceKey + " on Tab1");
|
||||
|
||||
EventUtils.synthesizeKey(advanceKey, { altKey: true, metaKey: true },
|
||||
browser2.contentWindow);
|
||||
EventUtils.synthesizeKey(advanceKey, { altKey: true, metaKey: true });
|
||||
is(gBrowser.selectedTab, tab3,
|
||||
"Tab3 should be activated by pressing Ctrl+" + advanceKey + " on Tab2");
|
||||
|
||||
EventUtils.synthesizeKey(reverseKey, { altKey: true, metaKey: true },
|
||||
browser3.contentWindow);
|
||||
EventUtils.synthesizeKey(reverseKey, { altKey: true, metaKey: true });
|
||||
is(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be activated by pressing Ctrl+" + reverseKey + " on Tab3");
|
||||
|
||||
EventUtils.synthesizeKey(reverseKey, { altKey: true, metaKey: true },
|
||||
browser2.contentWindow);
|
||||
EventUtils.synthesizeKey(reverseKey, { altKey: true, metaKey: true });
|
||||
is(gBrowser.selectedTab, tab1,
|
||||
"Tab1 should be activated by pressing Ctrl+" + reverseKey + " on Tab2");
|
||||
}
|
||||
|
@ -125,15 +113,13 @@ function test() {
|
|||
is(gBrowser.tabContainer.selectedIndex, 2,
|
||||
"Tab2 index should be 2");
|
||||
|
||||
EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true, shiftKey: true },
|
||||
browser2.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_PAGE_DOWN", { ctrlKey: true, shiftKey: true });
|
||||
is(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be activated after Ctrl+Shift+PageDown");
|
||||
is(gBrowser.tabContainer.selectedIndex, 3,
|
||||
"Tab2 index should be 1 after Ctrl+Shift+PageDown");
|
||||
|
||||
EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true, shiftKey: true },
|
||||
browser2.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_PAGE_UP", { ctrlKey: true, shiftKey: true });
|
||||
is(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be activated after Ctrl+Shift+PageUp");
|
||||
is(gBrowser.tabContainer.selectedIndex, 2,
|
||||
|
@ -153,31 +139,25 @@ function test() {
|
|||
|
||||
is(gBrowser.selectedTab, tab1,
|
||||
"Tab1 should be activated");
|
||||
EventUtils.synthesizeKey(advanceKey, { metaKey: true },
|
||||
browser1.contentWindow);
|
||||
|
||||
EventUtils.synthesizeKey(advanceKey, { metaKey: true });
|
||||
is(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be activated by pressing Ctrl+" + advanceKey + " on Tab1");
|
||||
|
||||
EventUtils.synthesizeKey(advanceKey, { metaKey: true },
|
||||
browser2.contentWindow);
|
||||
todo_is(gBrowser.selectedTab, tab3,
|
||||
EventUtils.synthesizeKey(advanceKey, { metaKey: true });
|
||||
is(gBrowser.selectedTab, tab3,
|
||||
"Tab3 should be activated by pressing Ctrl+" + advanceKey + " on Tab2");
|
||||
|
||||
if (gBrowser.selectedTab != tab3) {
|
||||
EventUtils.synthesizeKey(reverseKey, { metaKey: true },
|
||||
browser3.contentWindow);
|
||||
EventUtils.synthesizeKey(reverseKey, { metaKey: true });
|
||||
is(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be activated by pressing Ctrl+" + reverseKey + " on Tab3");
|
||||
}
|
||||
|
||||
EventUtils.synthesizeKey(reverseKey, { metaKey: true },
|
||||
browser2.contentWindow);
|
||||
todo_is(gBrowser.selectedTab, tab1,
|
||||
EventUtils.synthesizeKey(reverseKey, { metaKey: true });
|
||||
is(gBrowser.selectedTab, tab1,
|
||||
"Tab1 should be activated by pressing Ctrl+" + reverseKey + " on Tab2");
|
||||
} else {
|
||||
gBrowser.selectedTab = tab2;
|
||||
EventUtils.synthesizeKey("VK_F4", { type: "keydown", ctrlKey: true },
|
||||
browser2.contentWindow);
|
||||
EventUtils.synthesizeKey("VK_F4", { type: "keydown", ctrlKey: true });
|
||||
|
||||
isnot(gBrowser.selectedTab, tab2,
|
||||
"Tab2 should be closed by pressing Ctrl+F4 on Tab2");
|
||||
|
@ -188,8 +168,7 @@ function test() {
|
|||
gBrowser.getBrowserForTab(gBrowser.selectedTab).contentWindow;
|
||||
// NOTE: keypress event shouldn't be fired since the keydown event should
|
||||
// be consumed by tab2.
|
||||
EventUtils.synthesizeKey("VK_F4", { type: "keyup", ctrlKey: true },
|
||||
activeWindow);
|
||||
EventUtils.synthesizeKey("VK_F4", { type: "keyup", ctrlKey: true });
|
||||
is(gBrowser.tabs.length, 3,
|
||||
"The count of tabs should be 3 since renaming key events shouldn't close other tabs");
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -15,30 +15,22 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|||
XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
|
||||
"@mozilla.org/browser/aboutnewtab-service;1",
|
||||
"nsIAboutNewTabService");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
|
||||
"resource://gre/modules/Deprecated.jsm");
|
||||
|
||||
const DepecationURL = "https://bugzilla.mozilla.org/show_bug.cgi?id=1204983#c89";
|
||||
|
||||
this.NewTabURL = {
|
||||
|
||||
get: function() {
|
||||
Deprecated.warning("NewTabURL.get is deprecated, please query aboutNewTabService.newTabURL", DepecationURL);
|
||||
return aboutNewTabService.newTabURL;
|
||||
},
|
||||
|
||||
get overridden() {
|
||||
Deprecated.warning("NewTabURL.overridden is deprecated, please query aboutNewTabService.overridden", DepecationURL);
|
||||
return aboutNewTabService.overridden;
|
||||
},
|
||||
|
||||
override: function(newURL) {
|
||||
Deprecated.warning("NewTabURL.override is deprecated, please set aboutNewTabService.newTabURL", DepecationURL);
|
||||
aboutNewTabService.newTabURL = newURL;
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
Deprecated.warning("NewTabURL.reset is deprecated, please use aboutNewTabService.resetNewTabURL()", DepecationURL);
|
||||
aboutNewTabService.resetNewTabURL();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2974,9 +2974,6 @@ var DefaultBrowserCheck = {
|
|||
|
||||
#ifdef E10S_TESTING_ONLY
|
||||
var E10SUINotification = {
|
||||
// Increase this number each time we want to roll out an
|
||||
// e10s testing period to Nightly users.
|
||||
CURRENT_NOTICE_COUNT: 4,
|
||||
CURRENT_PROMPT_PREF: "browser.displayedE10SPrompt.1",
|
||||
PREVIOUS_PROMPT_PREF: "browser.displayedE10SPrompt",
|
||||
|
||||
|
@ -3007,20 +3004,7 @@ var E10SUINotification = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (Services.appinfo.browserTabsRemoteAutostart) {
|
||||
if (this.forcedOn) {
|
||||
return;
|
||||
}
|
||||
let notice = 0;
|
||||
try {
|
||||
notice = Services.prefs.getIntPref("browser.displayedE10SNotice");
|
||||
} catch(e) {}
|
||||
let activationNoticeShown = notice >= this.CURRENT_NOTICE_COUNT;
|
||||
|
||||
if (!activationNoticeShown) {
|
||||
this._showE10sActivatedNotice();
|
||||
}
|
||||
} else {
|
||||
if (!Services.appinfo.browserTabsRemoteAutostart) {
|
||||
let displayFeedbackRequest = false;
|
||||
try {
|
||||
displayFeedbackRequest = Services.prefs.getBoolPref("browser.requestE10sFeedback");
|
||||
|
@ -3032,10 +3016,6 @@ var E10SUINotification = {
|
|||
return;
|
||||
}
|
||||
|
||||
// The user has just voluntarily disabled e10s. Subtract one from displayedE10SNotice
|
||||
// so that the next time e10s is activated (either by the user or forced by us), they
|
||||
// can see the notice again.
|
||||
Services.prefs.setIntPref("browser.displayedE10SNotice", this.CURRENT_NOTICE_COUNT - 1);
|
||||
Services.prefs.clearUserPref("browser.requestE10sFeedback");
|
||||
|
||||
let url = Services.urlFormatter.formatURLPref("app.feedback.baseURL");
|
||||
|
@ -3084,32 +3064,6 @@ var E10SUINotification = {
|
|||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
|
||||
|
||||
_showE10sActivatedNotice: function() {
|
||||
let win = RecentWindow.getMostRecentBrowserWindow();
|
||||
if (!win)
|
||||
return;
|
||||
|
||||
Services.prefs.setIntPref("browser.displayedE10SNotice", this.CURRENT_NOTICE_COUNT);
|
||||
|
||||
let nb = win.document.getElementById("high-priority-global-notificationbox");
|
||||
let message = win.gNavigatorBundle.getFormattedString(
|
||||
"e10s.postActivationInfobar.message",
|
||||
[gBrandBundle.GetStringFromName("brandShortName")]
|
||||
);
|
||||
let buttons = [
|
||||
{
|
||||
label: win.gNavigatorBundle.getString("e10s.postActivationInfobar.learnMore.label"),
|
||||
accessKey: win.gNavigatorBundle.getString("e10s.postActivationInfobar.learnMore.accesskey"),
|
||||
callback: function () {
|
||||
win.openUILinkIn("https://wiki.mozilla.org/Electrolysis", "tab");
|
||||
}
|
||||
}
|
||||
];
|
||||
nb.appendNotification(message, "e10s-activated-noticed",
|
||||
null, nb.PRIORITY_WARNING_MEDIUM, buttons);
|
||||
|
||||
},
|
||||
|
||||
_showE10SPrompt: function BG__showE10SPrompt() {
|
||||
let win = RecentWindow.getMostRecentBrowserWindow();
|
||||
if (!win)
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
const nsICookie = Components.interfaces.nsICookie;
|
||||
|
||||
Components.utils.import("resource://gre/modules/PluralForm.jsm");
|
||||
Components.utils.import("resource://gre/modules/Services.jsm")
|
||||
Components.utils.import("resource:///modules/UserContextUI.jsm");
|
||||
|
||||
var gCookiesWindow = {
|
||||
_cm : Components.classes["@mozilla.org/cookiemanager;1"]
|
||||
|
@ -29,6 +31,10 @@ var gCookiesWindow = {
|
|||
this._populateList(true);
|
||||
|
||||
document.getElementById("filter").focus();
|
||||
|
||||
if (!Services.prefs.getBoolPref("privacy.userContext.enabled")) {
|
||||
document.getElementById("userContextRow").hidden = true;
|
||||
}
|
||||
},
|
||||
|
||||
uninit: function () {
|
||||
|
@ -462,7 +468,8 @@ var gCookiesWindow = {
|
|||
isSecure : aCookie.isSecure,
|
||||
expires : aCookie.expires,
|
||||
level : 1,
|
||||
container : false };
|
||||
container : false,
|
||||
originAttributes: aCookie.originAttributes };
|
||||
return c;
|
||||
},
|
||||
|
||||
|
@ -500,7 +507,7 @@ var gCookiesWindow = {
|
|||
|
||||
_updateCookieData: function (aItem) {
|
||||
var seln = this._view.selection;
|
||||
var ids = ["name", "value", "host", "path", "isSecure", "expires"];
|
||||
var ids = ["name", "value", "host", "path", "isSecure", "expires", "userContext"];
|
||||
var properties;
|
||||
|
||||
if (aItem && !aItem.container && seln.count > 0) {
|
||||
|
@ -509,18 +516,21 @@ var gCookiesWindow = {
|
|||
isDomain: aItem.isDomain ? this._bundle.getString("domainColon")
|
||||
: this._bundle.getString("hostColon"),
|
||||
isSecure: aItem.isSecure ? this._bundle.getString("forSecureOnly")
|
||||
: this._bundle.getString("forAnyConnection") };
|
||||
for (var i = 0; i < ids.length; ++i)
|
||||
: this._bundle.getString("forAnyConnection"),
|
||||
userContext: UserContextUI.getUserContextLabel(aItem.originAttributes.userContextId) };
|
||||
for (var i = 0; i < ids.length; ++i) {
|
||||
document.getElementById(ids[i]).disabled = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
var noneSelected = this._bundle.getString("noCookieSelected");
|
||||
properties = { name: noneSelected, value: noneSelected, host: noneSelected,
|
||||
path: noneSelected, expires: noneSelected,
|
||||
isSecure: noneSelected };
|
||||
for (i = 0; i < ids.length; ++i)
|
||||
isSecure: noneSelected, userContext: noneSelected };
|
||||
for (i = 0; i < ids.length; ++i) {
|
||||
document.getElementById(ids[i]).disabled = true;
|
||||
}
|
||||
}
|
||||
for (var property in properties)
|
||||
document.getElementById(property).value = properties[property];
|
||||
},
|
||||
|
|
|
@ -85,6 +85,10 @@
|
|||
<hbox pack="end"><label id="expiresLabel" control="expires" value="&props.expires.label;"/></hbox>
|
||||
<textbox id="expires" readonly="true" class="plain"/>
|
||||
</row>
|
||||
<row align="center" id="userContextRow">
|
||||
<hbox pack="end"><label id="userContextLabel" control="userContext" value="&props.container.label;"/></hbox>
|
||||
<textbox id="userContext" readonly="true" class="plain"/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
</hbox>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
}),
|
||||
];
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
},
|
||||
|
||||
// nsIWindowMediatorListener functions.
|
||||
|
@ -717,10 +730,10 @@ var WindowListener = {
|
|||
}, false);
|
||||
},
|
||||
|
||||
onCloseWindow: function(xulWindow) {
|
||||
onCloseWindow: function() {
|
||||
},
|
||||
|
||||
onWindowTitleChange: function(xulWindow, newTitle) {
|
||||
onWindowTitleChange: function() {
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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]))
|
|
@ -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);
|
|
@ -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");
|
|
@ -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.
|
||||
|
@ -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.
|
|
@ -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) {
|
|
@ -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");
|
||||
},
|
||||
|
||||
/**
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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>
|
|
@ -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 */
|
|
@ -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,25 +56,28 @@ 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 "room":
|
||||
{
|
||||
return React.createElement(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} />);
|
||||
case "failed":
|
||||
{
|
||||
return React.createElement(RoomFailureView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
failureReason: FAILURE_DETAILS.UNKNOWN });
|
||||
}
|
||||
default: {
|
||||
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);
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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() {
|
|
@ -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>
|
|
@ -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,36 +342,48 @@ describe("loop.panel", function() {
|
|||
|
||||
sinon.assert.calledOnce(requestStubs.LoginToFxA);
|
||||
});
|
||||
|
||||
it("should close the menu on clicking sign in", function() {
|
||||
var view = mountTestComponent();
|
||||
|
||||
TestUtils.Simulate.click(view.getDOMNode()
|
||||
.querySelector(".entry-settings-signin"));
|
||||
|
||||
expect(view.state.showMenu).eql(false);
|
||||
});
|
||||
|
||||
it("should close the panel on clicking sign in", function() {
|
||||
var view = mountTestComponent();
|
||||
|
||||
TestUtils.Simulate.click(view.getDOMNode()
|
||||
.querySelector(".entry-settings-signin"));
|
||||
|
||||
sinon.assert.calledOnce(fakeWindow.close);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UserLoggedIn", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
loop.storedRequests.GetUserProfile = { email: "test@example.com" };
|
||||
view = mountTestComponent();
|
||||
});
|
||||
|
||||
it("should show a signout entry when user is authenticated", function() {
|
||||
loop.storedRequests.GetUserProfile = { email: "test@example.com" };
|
||||
|
||||
var view = mountTestComponent();
|
||||
|
||||
sinon.assert.calledWithExactly(document.mozL10n.get,
|
||||
"settings_menu_item_signout");
|
||||
sinon.assert.neverCalledWith(document.mozL10n.get,
|
||||
"settings_menu_item_signin");
|
||||
expect(view.getDOMNode().querySelectorAll(".entry-settings-signout"))
|
||||
.to.have.length.of(1);
|
||||
expect(view.getDOMNode().querySelectorAll(".entry-settings-signin"))
|
||||
.to.have.length.of(0);
|
||||
});
|
||||
|
||||
it("should show an account entry when user is authenticated", function() {
|
||||
LoopMochaUtils.stubLoopRequest({
|
||||
GetUserProfile: function() { return { email: "test@example.com" }; }
|
||||
});
|
||||
|
||||
var view = mountTestComponent();
|
||||
|
||||
sinon.assert.calledWithExactly(document.mozL10n.get,
|
||||
"settings_menu_item_settings");
|
||||
expect(view.getDOMNode().querySelectorAll(".entry-settings-account"))
|
||||
.to.have.length.of(1);
|
||||
});
|
||||
|
||||
it("should open the FxA settings when the account entry is clicked",
|
||||
function() {
|
||||
loop.storedRequests.GetUserProfile = { email: "test@example.com" };
|
||||
|
||||
var view = mountTestComponent();
|
||||
|
||||
TestUtils.Simulate.click(view.getDOMNode()
|
||||
.querySelector(".entry-settings-account"));
|
||||
|
||||
|
@ -391,15 +391,33 @@ describe("loop.panel", function() {
|
|||
});
|
||||
|
||||
it("should sign out the user on click when authenticated", function() {
|
||||
loop.storedRequests.GetUserProfile = { email: "test@example.com" };
|
||||
var view = mountTestComponent();
|
||||
|
||||
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() {
|
||||
var view;
|
||||
|
||||
|
@ -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() {
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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")
|
|
@ -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() {
|
||||
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
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
До Ширина: | Высота: | Размер: 2.3 KiB После Ширина: | Высота: | Размер: 2.3 KiB |
До Ширина: | Высота: | Размер: 4.6 KiB После Ширина: | Высота: | Размер: 4.6 KiB |
До Ширина: | Высота: | Размер: 3.1 KiB После Ширина: | Высота: | Размер: 3.1 KiB |
До Ширина: | Высота: | Размер: 700 B После Ширина: | Высота: | Размер: 700 B |
До Ширина: | Высота: | Размер: 424 B После Ширина: | Высота: | Размер: 424 B |
До Ширина: | Высота: | Размер: 536 B После Ширина: | Высота: | Размер: 536 B |
До Ширина: | Высота: | Размер: 532 B После Ширина: | Высота: | Размер: 532 B |
До Ширина: | Высота: | Размер: 1.3 KiB После Ширина: | Высота: | Размер: 1.3 KiB |
До Ширина: | Высота: | Размер: 1.3 KiB После Ширина: | Высота: | Размер: 1.3 KiB |
До Ширина: | Высота: | Размер: 535 B После Ширина: | Высота: | Размер: 535 B |
До Ширина: | Высота: | Размер: 12 KiB После Ширина: | Высота: | Размер: 12 KiB |
До Ширина: | Высота: | Размер: 1.6 KiB После Ширина: | Высота: | Размер: 1.6 KiB |
До Ширина: | Высота: | Размер: 5.8 KiB После Ширина: | Высота: | Размер: 5.8 KiB |
До Ширина: | Высота: | Размер: 1.2 KiB После Ширина: | Высота: | Размер: 1.2 KiB |
До Ширина: | Высота: | Размер: 1.2 KiB После Ширина: | Высота: | Размер: 1.2 KiB |
До Ширина: | Высота: | Размер: 969 B После Ширина: | Высота: | Размер: 969 B |
До Ширина: | Высота: | Размер: 886 B После Ширина: | Высота: | Размер: 886 B |
До Ширина: | Высота: | Размер: 1.1 KiB После Ширина: | Высота: | Размер: 1.1 KiB |
До Ширина: | Высота: | Размер: 225 B После Ширина: | Высота: | Размер: 225 B |
До Ширина: | Высота: | Размер: 4.1 KiB После Ширина: | Высота: | Размер: 4.1 KiB |
До Ширина: | Высота: | Размер: 1.1 KiB После Ширина: | Высота: | Размер: 1.1 KiB |
До Ширина: | Высота: | Размер: 218 B После Ширина: | Высота: | Размер: 218 B |
До Ширина: | Высота: | Размер: 373 B После Ширина: | Высота: | Размер: 373 B |
До Ширина: | Высота: | Размер: 11 KiB После Ширина: | Высота: | Размер: 11 KiB |
До Ширина: | Высота: | Размер: 57 KiB После Ширина: | Высота: | Размер: 57 KiB |
До Ширина: | Высота: | Размер: 635 B После Ширина: | Высота: | Размер: 635 B |
До Ширина: | Высота: | Размер: 348 B После Ширина: | Высота: | Размер: 348 B |
До Ширина: | Высота: | Размер: 451 B После Ширина: | Высота: | Размер: 451 B |
До Ширина: | Высота: | Размер: 832 B После Ширина: | Высота: | Размер: 832 B |
До Ширина: | Высота: | Размер: 556 B После Ширина: | Высота: | Размер: 556 B |
До Ширина: | Высота: | Размер: 188 B После Ширина: | Высота: | Размер: 188 B |
До Ширина: | Высота: | Размер: 330 B После Ширина: | Высота: | Размер: 330 B |
До Ширина: | Высота: | Размер: 39 KiB После Ширина: | Высота: | Размер: 39 KiB |
До Ширина: | Высота: | Размер: 7.9 KiB После Ширина: | Высота: | Размер: 7.9 KiB |