Bug 1232707 - Import the latest version of the Loop system add-on. r=me for import of already reviewed code.

This commit is contained in:
Mark Banner
2016-01-14 16:07:21 +00:00
parent c9516f4991
commit 286e24c89d
90 changed files with 57026 additions and 2881 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.conversation = (function(mozL10n) {
loop.conversation = function (mozL10n) {
"use strict";
var sharedMixins = loop.shared.mixins;
@@ -18,41 +18,37 @@ loop.conversation = (function(mozL10n) {
* Master controller view for handling if incoming or outgoing calls are
* in progress, and hence, which view to display.
*/
var AppControllerView = React.createClass({displayName: "AppControllerView",
mixins: [
Backbone.Events,
loop.store.StoreMixin("conversationAppStore"),
sharedMixins.DocumentTitleMixin,
sharedMixins.WindowCloseMixin
],
var AppControllerView = React.createClass({
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 (React.createElement(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 (React.createElement(DesktopRoomConversationView, {
case "room":
{
return React.createElement(DesktopRoomConversationView, {
chatWindowDetached: this.state.chatWindowDetached,
dispatcher: this.props.dispatcher,
onCallTerminated: this.handleCallTerminated,
roomStore: this.props.roomStore}));
roomStore: this.props.roomStore });
}
case "failed": {
return (React.createElement(RoomFailureView, {
case "failed":
{
return React.createElement(RoomFailureView, {
dispatcher: this.props.dispatcher,
failureReason: FAILURE_DETAILS.UNKNOWN}));
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(
React.createElement(AppControllerView, {
React.render(React.createElement(AppControllerView, {
dispatcher: dispatcher,
roomStore: roomStore}), document.querySelector("#main"));
roomStore: roomStore }), document.querySelector("#main"));
document.documentElement.setAttribute("lang", mozL10n.getLanguage());
document.documentElement.setAttribute("dir", mozL10n.getDirection());
document.documentElement.setAttribute("lang", mozL10n.language.code);
document.documentElement.setAttribute("dir", mozL10n.language.direction);
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
dispatcher.dispatch(new sharedActions.GetWindowData({
@@ -222,6 +210,6 @@ loop.conversation = (function(mozL10n) {
*/
_sdkDriver: null
};
})(document.mozL10n);
}(document.mozL10n);
document.addEventListener("DOMContentLoaded", loop.conversation.init);

View File

@@ -3,14 +3,17 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.feedbackViews = (function(_, mozL10n) {
loop.feedbackViews = function (_, mozL10n) {
"use strict";
/**
* Feedback view is displayed once every 6 months (loop.feedback.periodSec)
* after a conversation has ended.
*/
var FeedbackView = React.createClass({displayName: "FeedbackView",
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 (
React.createElement("div", {className: "feedback-view-container"},
React.createElement("h2", {className: "feedback-heading"},
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"},
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);

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -3,7 +3,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.roomViews = (function(mozL10n) {
loop.roomViews = function (mozL10n) {
"use strict";
var ROOM_STATES = loop.store.ROOM_STATES;
@@ -24,20 +24,17 @@ loop.roomViews = (function(mozL10n) {
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);
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() {
componentWillUnmount: function () {
this.stopListening(this.props.roomStore);
},
_onActiveRoomStateChanged: function() {
_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.
@@ -46,7 +43,7 @@ loop.roomViews = (function(mozL10n) {
}
},
_onRoomError: function() {
_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.
@@ -55,7 +52,7 @@ loop.roomViews = (function(mozL10n) {
}
},
_onRoomSavingContext: function() {
_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.
@@ -64,7 +61,7 @@ loop.roomViews = (function(mozL10n) {
}
},
getInitialState: function() {
getInitialState: function () {
var storeState = this.props.roomStore.getStoreState("activeRoom");
return _.extend({
// Used by the UI showcase.
@@ -77,7 +74,9 @@ loop.roomViews = (function(mozL10n) {
/**
* Used to display errors in direct calls and rooms to the user.
*/
var FailureInfoView = React.createClass({displayName: "FailureInfoView",
var FailureInfoView = React.createClass({
displayName: "FailureInfoView",
propTypes: {
failureReason: React.PropTypes.string.isRequired
},
@@ -87,14 +86,13 @@ loop.roomViews = (function(mozL10n) {
*
* @return {String} The translated message for the failure reason.
*/
_getMessage: function() {
_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") });
return mozL10n.get("tos_failure_message", { clientShortname: mozL10n.get("clientShortname2") });
case FAILURE_DETAILS.ICE_FAILED:
return mozL10n.get("ice_failure_message");
default:
@@ -102,11 +100,15 @@ loop.roomViews = (function(mozL10n) {
}
},
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())
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()
)
);
}
@@ -115,7 +117,9 @@ loop.roomViews = (function(mozL10n) {
/**
* Something went wrong view. Displayed when there's a big problem.
*/
var RoomFailureView = React.createClass({displayName: "RoomFailureView",
var RoomFailureView = React.createClass({
displayName: "RoomFailureView",
mixins: [sharedMixins.AudioMixin],
propTypes: {
@@ -123,19 +127,15 @@ loop.roomViews = (function(mozL10n) {
failureReason: React.PropTypes.string
},
componentDidMount: function() {
componentDidMount: function () {
this.play("failure");
},
handleRejoinCall: function() {
handleRejoinCall: function () {
this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
},
render: function() {
var settingsMenuItems = [
{ id: "help" }
];
render: function () {
var btnTitle;
if (this.props.failureReason === FAILURE_DETAILS.ICE_FAILED) {
btnTitle = mozL10n.get("retry_call_button");
@@ -143,24 +143,27 @@ loop.roomViews = (function(mozL10n) {
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},
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
)
),
React.createElement(loop.shared.views.SettingsControlButton, {
menuBelow: true,
menuItems: settingsMenuItems})
)
);
}
});
var SocialShareDropdown = React.createClass({displayName: "SocialShareDropdown",
var SocialShareDropdown = React.createClass({
displayName: "SocialShareDropdown",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomUrl: React.PropTypes.string,
@@ -168,18 +171,17 @@ loop.roomViews = (function(mozL10n) {
socialShareProviders: React.PropTypes.array
},
handleAddServiceClick: function(event) {
handleAddServiceClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
},
handleProviderClick: function(event) {
handleProviderClick: function (event) {
event.preventDefault();
var origin = event.currentTarget.dataset.provider;
var provider = this.props.socialShareProviders
.filter(function(socialProvider) {
var provider = this.props.socialShareProviders.filter(function (socialProvider) {
return socialProvider.origin === origin;
})[0];
@@ -190,7 +192,7 @@ loop.roomViews = (function(mozL10n) {
}));
},
render: function() {
render: function () {
// Don't render a thing when no data has been fetched yet.
if (!this.props.socialShareProviders) {
return null;
@@ -204,27 +206,35 @@ loop.roomViews = (function(mozL10n) {
"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"))
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",
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)
onClick: this.handleProviderClick },
React.createElement("img", { className: "icon", src: provider.iconURL }),
React.createElement(
"span",
null,
provider.name
)
);
}.bind(this))
)
);
}
});
@@ -232,7 +242,9 @@ loop.roomViews = (function(mozL10n) {
/**
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({displayName: "DesktopRoomInvitationView",
var DesktopRoomInvitationView = React.createClass({
displayName: "DesktopRoomInvitationView",
statics: {
TRIGGERED_RESET_DELAY: 2000
},
@@ -242,24 +254,20 @@ loop.roomViews = (function(mozL10n) {
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
onAddContextClick: React.PropTypes.func,
onEditContextClose: React.PropTypes.func,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
savingContext: React.PropTypes.bool,
show: React.PropTypes.bool.isRequired,
showEditContext: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array
},
getInitialState: function() {
getInitialState: function () {
return {
copiedUrl: false,
newRoomName: ""
};
},
handleEmailButtonClick: function(event) {
handleEmailButtonClick: function (event) {
event.preventDefault();
var roomData = this.props.roomData;
@@ -272,15 +280,14 @@ loop.roomViews = (function(mozL10n) {
}
}
this.props.dispatcher.dispatch(
new sharedActions.EmailRoomUrl({
this.props.dispatcher.dispatch(new sharedActions.EmailRoomUrl({
roomUrl: roomData.roomUrl,
roomDescription: contextURL,
from: "conversation"
}));
},
handleFacebookButtonClick: function(event) {
handleFacebookButtonClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.FacebookShareRoomUrl({
@@ -289,7 +296,7 @@ loop.roomViews = (function(mozL10n) {
}));
},
handleCopyButtonClick: function(event) {
handleCopyButtonClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
@@ -304,57 +311,84 @@ loop.roomViews = (function(mozL10n) {
/**
* Reset state of triggered buttons if necessary
*/
resetTriggeredButtons: function() {
resetTriggeredButtons: function () {
if (this.state.copiedUrl) {
this.setState({ copiedUrl: false });
}
},
handleEditContextClose: function() {
if (this.props.onEditContextClose) {
this.props.onEditContextClose();
}
},
render: function() {
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", {className: cx({ hide: this.props.showEditContext })},
mozL10n.get("invite_header_text2")
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({
React.createElement(
"div",
{ className: cx({
"btn-group": true,
"call-action-group": true,
hide: this.props.showEditContext
})},
React.createElement("div", {className: cx({
"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"))
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",
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"))
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",
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"))
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, {
@@ -362,222 +396,7 @@ loop.roomViews = (function(mozL10n) {
ref: "menu",
roomUrl: this.props.roomData.roomUrl,
show: this.state.showMenu,
socialShareProviders: this.props.socialShareProviders}),
React.createElement(DesktopRoomEditContextView, {
dispatcher: this.props.dispatcher,
error: this.props.error,
onClose: this.handleEditContextClose,
roomData: this.props.roomData,
savingContext: this.props.savingContext,
show: this.props.showEditContext})
)
);
}
});
var DesktopRoomEditContextView = React.createClass({displayName: "DesktopRoomEditContextView",
mixins: [React.addons.LinkedStateMixin],
maxRoomNameLength: 124,
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
onClose: React.PropTypes.func,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
savingContext: React.PropTypes.bool.isRequired,
show: React.PropTypes.bool.isRequired
},
componentWillMount: function() {
this._fetchMetadata();
},
componentWillReceiveProps: function(nextProps) {
var newState = {};
// When the 'show' prop is changed from outside this component, we do need
// to update the state.
if (("show" in nextProps) && nextProps.show !== this.props.show) {
newState.show = nextProps.show;
if (nextProps.show) {
this._fetchMetadata();
}
}
// When we receive an update for the `roomData` property, make sure that
// the current form fields reflect reality. This is necessary, because the
// form state is maintained in the components' state.
if (nextProps.roomData) {
// Right now it's only necessary to update the form input states when
// they contain no text yet.
if (!this.state.newRoomName && nextProps.roomData.roomName) {
newState.newRoomName = nextProps.roomData.roomName;
}
var url = this._getURL(nextProps.roomData);
if (url) {
if (!this.state.newRoomURL && url.location) {
newState.newRoomURL = url.location;
}
if (!this.state.newRoomDescription && url.description) {
newState.newRoomDescription = url.description;
}
if (!this.state.newRoomThumbnail && url.thumbnail) {
newState.newRoomThumbnail = url.thumbnail;
}
}
}
// Feature support: when a context save completed without error, we can
// close the context edit form.
if (("savingContext" in nextProps) && this.props.savingContext &&
this.props.savingContext !== nextProps.savingContext && this.state.show
&& !this.props.error && !nextProps.error) {
newState.show = false;
if (this.props.onClose) {
this.props.onClose();
}
}
if (Object.getOwnPropertyNames(newState).length) {
this.setState(newState);
}
},
getInitialState: function() {
var url = this._getURL();
return {
// `availableContext` prop only used in tests.
availableContext: null,
show: this.props.show,
newRoomName: this.props.roomData.roomName || "",
newRoomURL: url && url.location || "",
newRoomDescription: url && url.description || "",
newRoomThumbnail: url && url.thumbnail || ""
};
},
_fetchMetadata: function() {
loop.request("GetSelectedTabMetadata").then(function(metadata) {
var previewImage = metadata.favicon || "";
var description = metadata.title || metadata.description;
var metaUrl = metadata.url;
this.setState({
availableContext: {
previewImage: previewImage,
description: description,
url: metaUrl
}
});
}.bind(this));
},
handleCloseClick: function(event) {
event.stopPropagation();
event.preventDefault();
this.setState({ show: false });
if (this.props.onClose) {
this.props.onClose();
}
},
handleContextClick: function(event) {
event.stopPropagation();
event.preventDefault();
var url = this._getURL();
if (!url || !url.location) {
return;
}
loop.requestMulti(
["OpenURL", url.location],
["TelemetryAddValue", "LOOP_ROOM_CONTEXT_CLICK", 1]);
},
handleFormSubmit: function(event) {
event && event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.UpdateRoomContext({
roomToken: this.props.roomData.roomToken,
newRoomName: this.state.newRoomName,
newRoomURL: this.state.newRoomURL,
newRoomDescription: this.state.newRoomDescription,
newRoomThumbnail: this.state.newRoomThumbnail
}));
},
handleTextareaKeyDown: function(event) {
// Submit the form as soon as the user press Enter in that field
// Note: We're using a textarea instead of a simple text input to display
// placeholder and entered text on two lines, to circumvent l10n
// rendering/UX issues for some locales.
if (event.which === 13) {
this.handleFormSubmit(event);
}
},
/**
* Utility function to extract URL context data from the `roomData` property
* that can also be supplied as an argument.
*
* @param {Object} roomData Optional room data object to use, equivalent to
* the activeRoomStore state.
* @return {Object} The first context URL found on the `roomData` object.
*/
_getURL: function(roomData) {
roomData = roomData || this.props.roomData;
return this.props.roomData.roomContextUrls &&
this.props.roomData.roomContextUrls[0];
},
render: function() {
if (!this.state.show) {
return null;
}
var url = this._getURL();
var thumbnail = url && url.thumbnail || "shared/img/icons-16x16.svg#globe";
var urlDescription = url && url.description || "";
var location = url && url.location || "";
var cx = classNames;
var availableContext = this.state.availableContext;
return (
React.createElement("div", {className: "room-context"},
React.createElement("p", {className: cx({ "error": !!this.props.error,
"error-display-area": true })},
mozL10n.get("rooms_change_failed_label")
),
React.createElement("h2", {className: "room-context-header"}, mozL10n.get("context_inroom_header")),
React.createElement("form", {onSubmit: this.handleFormSubmit},
React.createElement("input", {className: "room-context-name",
maxLength: this.maxRoomNameLength,
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("context_edit_name_placeholder"),
type: "text",
valueLink: this.linkState("newRoomName")}),
React.createElement("input", {className: "room-context-url",
disabled: availableContext && availableContext.url === this.state.newRoomURL,
onKeyDown: this.handleTextareaKeyDown,
placeholder: "https://",
type: "text",
valueLink: this.linkState("newRoomURL")}),
React.createElement("textarea", {className: "room-context-comments",
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("context_edit_comments_placeholder"),
rows: "2", type: "text",
valueLink: this.linkState("newRoomDescription")}),
React.createElement(sharedViews.ButtonGroup, null,
React.createElement(sharedViews.Button, {additionalClass: "button-cancel",
caption: mozL10n.get("context_cancel_label"),
onClick: this.handleCloseClick}),
React.createElement(sharedViews.Button, {additionalClass: "button-accept",
caption: mozL10n.get("context_done_label"),
disabled: this.props.savingContext,
onClick: this.handleFormSubmit})
)
)
)
socialShareProviders: this.props.socialShareProviders })
);
}
});
@@ -585,14 +404,10 @@ loop.roomViews = (function(mozL10n) {
/**
* Desktop room conversation view.
*/
var DesktopRoomConversationView = React.createClass({displayName: "DesktopRoomConversationView",
mixins: [
ActiveRoomStoreMixin,
sharedMixins.DocumentTitleMixin,
sharedMixins.MediaSetupMixin,
sharedMixins.RoomsAudioMixin,
sharedMixins.WindowCloseMixin
],
var DesktopRoomConversationView = React.createClass({
displayName: "DesktopRoomConversationView",
mixins: [ActiveRoomStoreMixin, sharedMixins.DocumentTitleMixin, sharedMixins.MediaSetupMixin, sharedMixins.RoomsAudioMixin, sharedMixins.WindowCloseMixin],
propTypes: {
chatWindowDetached: React.PropTypes.bool.isRequired,
@@ -604,18 +419,11 @@ loop.roomViews = (function(mozL10n) {
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
},
getInitialState: function() {
return {
showEditContext: false
};
},
componentWillUpdate: function(nextProps, nextState) {
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) {
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
@@ -624,8 +432,7 @@ loop.roomViews = (function(mozL10n) {
}
// 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) {
if (this.state.roomState !== ROOM_STATES.SESSION_CONNECTED && nextState.roomState === ROOM_STATES.SESSION_CONNECTED) {
this.props.dispatcher.dispatch(new sharedActions.StartBrowserShare());
}
},
@@ -633,7 +440,7 @@ loop.roomViews = (function(mozL10n) {
/**
* User clicked on the "Leave" button.
*/
leaveRoom: function() {
leaveRoom: function () {
if (this.state.used) {
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
} else {
@@ -647,9 +454,8 @@ loop.roomViews = (function(mozL10n) {
* @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({
publishStream: function (type, enabled) {
this.props.dispatcher.dispatch(new sharedActions.SetMute({
type: type,
enabled: enabled
}));
@@ -660,9 +466,8 @@ loop.roomViews = (function(mozL10n) {
*
* @return {Boolean} True if there's no guests.
*/
_shouldRenderInvitationOverlay: function() {
var hasGuests = typeof this.state.participants === "object" &&
this.state.participants.filter(function(participant) {
_shouldRenderInvitationOverlay: function () {
var hasGuests = typeof this.state.participants === "object" && this.state.participants.filter(function (participant) {
return !participant.owner;
}).length > 0;
@@ -680,7 +485,7 @@ loop.roomViews = (function(mozL10n) {
* XXX Refactor shouldRenderRemoteVideo & shouldRenderLoading into one fn
* that returns an enum
*/
shouldRenderRemoteVideo: function() {
shouldRenderRemoteVideo: function () {
switch (this.state.roomState) {
case ROOM_STATES.HAS_PARTICIPANTS:
if (this.state.remoteVideoEnabled) {
@@ -710,8 +515,7 @@ loop.roomViews = (function(mozL10n) {
return true;
default:
console.warn("DesktopRoomConversationView.shouldRenderRemoteVideo:" +
" unexpected roomState: ", this.state.roomState);
console.warn("DesktopRoomConversationView.shouldRenderRemoteVideo:" + " unexpected roomState: ", this.state.roomState);
return true;
}
},
@@ -723,9 +527,8 @@ loop.roomViews = (function(mozL10n) {
* @returns {boolean}
* @private
*/
_isLocalLoading: function() {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
!this.state.localSrcMediaElement;
_isLocalLoading: function () {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT && !this.state.localSrcMediaElement;
},
/**
@@ -735,78 +538,55 @@ loop.roomViews = (function(mozL10n) {
* @returns {boolean}
* @private
*/
_isRemoteLoading: function() {
return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
!this.state.remoteSrcMediaElement &&
!this.state.mediaConnected);
_isRemoteLoading: function () {
return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS && !this.state.remoteSrcMediaElement && !this.state.mediaConnected);
},
handleAddContextClick: function() {
this.setState({ showEditContext: true });
},
handleEditContextClick: function() {
this.setState({ showEditContext: !this.state.showEditContext });
},
handleEditContextClose: function() {
this.setState({ showEditContext: false });
},
componentDidUpdate: function(prevProps, prevState) {
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) {
if (prevState.roomState === ROOM_STATES.ENDED && this.state.roomState === ROOM_STATES.ENDED) {
this.props.onCallTerminated();
}
},
handleContextMenu: function(e) {
handleContextMenu: function (e) {
e.preventDefault();
},
render: function() {
render: function () {
if (this.state.roomName || this.state.roomContextUrls) {
var roomTitle = this.state.roomName ||
this.state.roomContextUrls[0].description ||
this.state.roomContextUrls[0].location;
var roomTitle = this.state.roomName || this.state.roomContextUrls[0].description || this.state.roomContextUrls[0].location;
this.setTitle(roomTitle);
}
var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
var shouldRenderEditContextView = this.state.showEditContext;
var roomData = this.props.roomStore.getStoreState("activeRoom");
switch (this.state.roomState) {
case ROOM_STATES.FAILED:
case ROOM_STATES.FULL: {
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, {
return React.createElement(RoomFailureView, {
dispatcher: this.props.dispatcher,
failureReason: this.state.failureReason})
);
failureReason: this.state.failureReason });
}
case ROOM_STATES.ENDED: {
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: {
var settingsMenuItems = [
default:
{
return React.createElement(
"div",
{ className: "room-conversation-wrapper desktop-room-wrapper",
onContextMenu: this.handleContextMenu },
React.createElement(
sharedViews.MediaLayoutView,
{
id: "edit",
enabled: !this.state.showEditContext,
visible: true,
onClick: this.handleEditContextClick
},
{ id: "help" }
];
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(),
@@ -822,34 +602,20 @@ loop.roomViews = (function(mozL10n) {
screenShareMediaElement: this.state.screenShareMediaElement,
screenSharePosterUrl: null,
showInitialContext: false,
useDesktopPaths: true},
useDesktopPaths: true },
React.createElement(sharedViews.ConversationToolbar, {
audio: { enabled: !this.state.audioMuted, visible: true},
audio: { enabled: !this.state.audioMuted, visible: true },
dispatcher: this.props.dispatcher,
hangup: this.leaveRoom,
publishStream: this.publishStream,
settingsMenuItems: settingsMenuItems,
show: !shouldRenderEditContextView,
showHangup: this.props.chatWindowDetached,
video: { enabled: !this.state.videoMuted, visible: true}}),
video: { enabled: !this.state.videoMuted, visible: true } }),
React.createElement(DesktopRoomInvitationView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
onAddContextClick: this.handleAddContextClick,
onEditContextClose: this.handleEditContextClose,
roomData: roomData,
savingContext: this.state.savingContext,
show: shouldRenderInvitationOverlay,
showEditContext: shouldRenderInvitationOverlay && shouldRenderEditContextView,
socialShareProviders: this.state.socialShareProviders}),
React.createElement(DesktopRoomEditContextView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
onClose: this.handleEditContextClose,
roomData: roomData,
savingContext: this.state.savingContext,
show: !shouldRenderInvitationOverlay && shouldRenderEditContextView})
)
socialShareProviders: this.state.socialShareProviders })
)
);
}
@@ -862,9 +628,7 @@ loop.roomViews = (function(mozL10n) {
FailureInfoView: FailureInfoView,
RoomFailureView: RoomFailureView,
SocialShareDropdown: SocialShareDropdown,
DesktopRoomEditContextView: DesktopRoomEditContextView,
DesktopRoomConversationView: DesktopRoomConversationView,
DesktopRoomInvitationView: DesktopRoomInvitationView
};
})(document.mozL10n || navigator.mozL10n);
}(document.mozL10n || navigator.mozL10n);

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<head>
<meta charset="utf-8">
<title>Loop desktop-local mocha tests</title>
<link rel="stylesheet" media="all" href="../shared/vendor/mocha-2.2.5.css">
<link rel="stylesheet" media="all" href="/test/vendor/mocha.css">
</head>
<body>
<div id="mocha">
@@ -14,22 +14,26 @@
</div>
<div id="messages"></div>
<div id="fixtures"></div>
<script src="../../content/shared/vendor/lodash-3.9.3.js"></script>
<script src="../shared/loop_mocha_utils.js"></script>
<script src="/shared/vendor/lodash.js"></script>
<script src="/shared/test/loop_mocha_utils.js"></script>
<script>
LoopMochaUtils.trapErrors();
</script>
<!-- libs -->
<script src="../../content/panels/vendor/l10n.js"></script>
<script src="../../content/shared/vendor/react-0.13.3.js"></script>
<script src="../../content/shared/vendor/classnames-2.2.0.js"></script>
<script src="../../content/shared/vendor/backbone-1.2.1.js"></script>
<!-- test dependencies -->
<script src="../shared/vendor/mocha-2.2.5.js"></script>
<script src="../shared/vendor/chai-3.0.0.js"></script>
<script src="../shared/vendor/sinon-1.16.1.js"></script>
<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;
@@ -37,25 +41,25 @@
</script>
<!-- App scripts -->
<script src="../../content/shared/js/loopapi-client.js"></script>
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../content/shared/js/store.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
<script src="../../content/panels/js/conversationAppStore.js"></script>
<script src="../../content/panels/js/roomStore.js"></script>
<script src="../../content/panels/js/roomViews.js"></script>
<script src="../../content/panels/js/feedbackViews.js"></script>
<script src="../../content/panels/js/conversation.js"></script>
<script src="../../content/panels/js/panel.js"></script>
<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>

View File

@@ -8,13 +8,11 @@ describe("loop.panel", function() {
var expect = chai.expect;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sandbox, notifications, requestStubs;
var fakeXHR, fakeWindow, fakeEvent;
var requests = [];
var roomData, roomData2, roomList, roomName;
var mozL10nGetSpy;
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
@@ -56,13 +54,16 @@ describe("loop.panel", function() {
GetPluralRule: sinon.stub(),
SetLoopPref: sinon.stub(),
GetLoopPref: function(prefName) {
if (prefName === "debug.dispatcher") {
return false;
}
return 1;
},
SetPanelHeight: function() { return null; },
GetPluralForm: function() {
return "fakeText";
},
"Rooms:GetAll": function(version) {
"Rooms:GetAll": function() {
return [];
},
"Rooms:PushSubscription": sinon.stub(),
@@ -184,14 +185,9 @@ describe("loop.panel", function() {
});
describe("loop.panel.PanelView", function() {
var dispatcher, roomStore, callUrlData;
var dispatcher, roomStore;
beforeEach(function() {
callUrlData = {
callUrl: "http://call.invalid/",
expiresAt: 1000
};
dispatcher = new loop.Dispatcher();
roomStore = new loop.store.RoomStore(dispatcher, {
constants: {}
@@ -216,7 +212,7 @@ describe("loop.panel", function() {
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.SettingsDropdown));
expect(view.getDOMNode().querySelectorAll(".icon-account"))
expect(view.getDOMNode().querySelectorAll(".entry-settings-account"))
.to.have.length.of(0);
});
@@ -266,19 +262,11 @@ describe("loop.panel", function() {
expect(view.getDOMNode()).to.be.null;
});
it("should add ellipsis to text over 24chars", function() {
loop.storedRequests.GetUserProfile = { email: "reallyreallylongtext@example.com" };
var view = createTestPanelView();
var node = view.getDOMNode().querySelector(".user-identity");
expect(node.textContent).to.eql("reallyreallylongtext@exa…");
});
it("should warn when user profile is different from {} or null",
function() {
var warnstub = sandbox.stub(console, "warn");
var view = TestUtils.renderIntoDocument(React.createElement(
TestUtils.renderIntoDocument(React.createElement(
loop.panel.AccountLink, {
fxAEnabled: false,
userProfile: []
@@ -294,7 +282,7 @@ describe("loop.panel", function() {
function() {
var warnstub = sandbox.stub(console, "warn");
var view = TestUtils.renderIntoDocument(React.createElement(
TestUtils.renderIntoDocument(React.createElement(
loop.panel.AccountLink, {
fxAEnabled: false,
userProfile: {}
@@ -311,7 +299,7 @@ describe("loop.panel", function() {
React.createElement(loop.panel.SettingsDropdown));
}
var loginToFxAStub, logoutFromFxAStub, openFxASettingsStub;
var openFxASettingsStub;
beforeEach(function() {
openFxASettingsStub = sandbox.stub();
@@ -342,7 +330,7 @@ describe("loop.panel", function() {
function() {
var view = mountTestComponent();
expect(view.getDOMNode().querySelectorAll(".icon-account"))
expect(view.getDOMNode().querySelectorAll(".entry-settings-account"))
.to.have.length.of(0);
});
@@ -354,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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -326,6 +326,7 @@ loop.shared.actions = (function() {
// newRoomDescription: String, Optional.
// newRoomThumbnail: String, Optional.
// newRoomURL: String Optional.
// sentTimestamp: String, Optional.
}),
/**

View File

@@ -385,7 +385,7 @@ loop.store.ActiveRoomStore = (function() {
* and decryption is complete.
*/
_getRoomDataForStandalone: function(roomCryptoKey) {
return new Promise(function(resolve, reject) {
return new Promise(function(resolve) {
loop.request("Rooms:Get", this._storeState.roomToken).then(function(result) {
if (result.isError) {
resolve(new sharedActions.RoomFailure({
@@ -438,7 +438,7 @@ loop.store.ActiveRoomStore = (function() {
roomInfoData.roomName = realResult.roomName;
resolve(roomInfoData);
}, function(error) {
}, function() {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.DECRYPT_FAILED;
resolve(roomInfoData);
});
@@ -454,7 +454,7 @@ loop.store.ActiveRoomStore = (function() {
* if Firefox can handle the room.
*/
_promiseDetectUserAgentHandles: function() {
return new Promise(function(resolve, reject) {
return new Promise(function(resolve) {
function resolveWithNotHandlingResponse() {
resolve(new sharedActions.UserAgentHandlesRoom({
handlesRoom: false
@@ -568,9 +568,8 @@ loop.store.ActiveRoomStore = (function() {
/**
* Handles the deletion of a room, notified by the Loop rooms API.
*
* @param {Object} roomData The roomData of the deleted room
*/
_handleRoomDelete: function(roomData) {
_handleRoomDelete: function() {
this._sdkDriver.forceDisconnectAll(function() {
window.close();
});
@@ -926,7 +925,8 @@ loop.store.ActiveRoomStore = (function() {
// The browser being shared changed, so update to the new context
loop.request("GetSelectedTabMetadata").then(function(meta) {
if (!meta) {
// Avoid sending the event if there is no data nor participants nor url
if (!meta || !meta.url || !this._hasParticipants()) {
return;
}
@@ -951,7 +951,7 @@ loop.store.ActiveRoomStore = (function() {
*
* @param {sharedActions.StartBrowserShare} actionData
*/
startBrowserShare: function(actionData) {
startBrowserShare: function() {
// For the unit test we already set the state here, instead of indirectly
// via an action, because actions are queued thus depending on the
// asynchronous nature of `loop.request`.
@@ -960,9 +960,6 @@ loop.store.ActiveRoomStore = (function() {
state: SCREEN_SHARE_STATES.PENDING
}));
var options = {
videoSource: "browser"
};
this._browserSharingListener = this._handleSwitchBrowserShare.bind(this);
// Set up a listener for watching screen shares. This will get notified
@@ -1224,6 +1221,24 @@ loop.store.ActiveRoomStore = (function() {
*/
sendTextChatMessage: function(actionData) {
this._handleTextChatMessage(actionData);
},
/**
* Checks if the room is empty or has participants.
*
*/
_hasParticipants: function() {
// Update the participants to just the owner.
var participants = this.getStoreState("participants");
if (participants) {
participants = participants.filter(function(participant) {
return !participant.owner;
});
return participants.length > 0;
}
return false;
}
});

View File

@@ -1,3 +1,5 @@
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* 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/. */
@@ -5,7 +7,7 @@
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.LinkifiedTextView = (function() {
loop.shared.views.LinkifiedTextView = function () {
"use strict";
/**
@@ -13,7 +15,10 @@ loop.shared.views.LinkifiedTextView = (function() {
* links starting with http://, https://, or ftp:// as actual clickable
* links inside a <p> container.
*/
var LinkifiedTextView = React.createClass({displayName: "LinkifiedTextView",
var LinkifiedTextView = React.createClass({
displayName: "LinkifiedTextView",
propTypes: {
// Call this instead of allowing the default <a> click semantics, if
// given. Also causes sendReferrer and suppressTarget attributes to be
@@ -28,18 +33,16 @@ loop.shared.views.LinkifiedTextView = (function() {
suppressTarget: React.PropTypes.bool
},
mixins: [
React.addons.PureRenderMixin
],
mixins: [React.addons.PureRenderMixin],
_handleClickEvent: function(e) {
_handleClickEvent: function (e) {
e.preventDefault();
e.stopPropagation();
this.props.linkClickHandler(e.currentTarget.href);
},
_generateLinkAttributes: function(href) {
_generateLinkAttributes: function (href) {
var linkAttributes = {
href: href
};
@@ -71,7 +74,7 @@ loop.shared.views.LinkifiedTextView = (function() {
*
* @returns {Array} of strings and React <a> elements in order.
*/
parseStringToElements: function(s) {
parseStringToElements: function (s) {
var elements = [];
var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
var reactElementsCounter = 0; // For giving keys to each ReactElement.
@@ -85,12 +88,12 @@ loop.shared.views.LinkifiedTextView = (function() {
}
// Push the first link itself, and advance the string pointer again.
elements.push(
React.createElement("a", React.__spread({}, this._generateLinkAttributes(result[0]) ,
{key: reactElementsCounter++}),
elements.push(React.createElement(
"a",
_extends({}, this._generateLinkAttributes(result[0]), {
key: reactElementsCounter++ }),
result[0]
)
);
));
s = s.substr(result[0].length);
// Check for another link, and perhaps continue...
@@ -104,13 +107,14 @@ loop.shared.views.LinkifiedTextView = (function() {
return elements;
},
render: function() {
return (
React.createElement("p", null, this.parseStringToElements(this.props.rawText))
render: function () {
return React.createElement(
"p",
null,
this.parseStringToElements(this.props.rawText)
);
}
});
return LinkifiedTextView;
})();
}();

View File

@@ -133,11 +133,12 @@ loop.OTSdkDriver = (function() {
// the initial connect of the session. This saves time when setting up
// the media.
this.publisher = this.sdk.initPublisher(this._mockPublisherEl,
_.extend(this._getDataChannelSettings, this._getCopyPublisherConfig));
_.extend(this._getDataChannelSettings, this._getCopyPublisherConfig),
this._onPublishComplete.bind(this));
this.publisher.on("streamCreated", this._onLocalStreamCreated.bind(this));
this.publisher.on("streamDestroyed", this._onLocalStreamDestroyed.bind(this));
this.publisher.on("accessAllowed", this._onPublishComplete.bind(this));
this.publisher.on("accessAllowed", this._onPublishAllowed.bind(this));
this.publisher.on("accessDenied", this._onPublishDenied.bind(this));
this.publisher.on("accessDialogOpened",
this._onAccessDialogOpened.bind(this));
@@ -185,9 +186,9 @@ loop.OTSdkDriver = (function() {
this._mockScreenSharePreviewEl = document.createElement("div");
this.screenshare = this.sdk.initPublisher(this._mockScreenSharePreviewEl,
config);
config, this._onScreenSharePublishComplete.bind(this));
this.screenshare.on("accessAllowed", this._onScreenShareGranted.bind(this));
this.screenshare.on("accessDenied", this._onScreenShareDenied.bind(this));
this.screenshare.on("accessDenied", this._onScreenSharePublishError.bind(this));
this.screenshare.on("streamCreated", this._onScreenShareStreamCreated.bind(this));
this._noteSharingState(options.videoSource, true);
@@ -902,7 +903,7 @@ loop.OTSdkDriver = (function() {
*
* @param {OT.Event} event
*/
_onPublishComplete: function(event) {
_onPublishAllowed: function(event) {
event.preventDefault();
this._publisherReady = true;
@@ -911,6 +912,33 @@ loop.OTSdkDriver = (function() {
this._maybePublishLocalStream();
},
/**
*
* Handles publisher Complete.
*
* @param {Error} error An OT error object, null if there was no error.
*/
_onPublishComplete: function(error) {
if (!error) {
// Nothing to do for the success case.
return;
}
if (!(error.message && error.message === "DENIED")) {
// We free up the publisher here in case the store wants to try
// grabbing the media again.
if (this.publisher) {
this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated");
this.publisher.destroy();
delete this.publisher;
delete this._mockPublisherEl;
}
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
this._notifyMetricsEvent("sdk.exception." + error.code + "." + error.message);
}
},
/**
* Handles publishing of media being denied.
*
@@ -936,26 +964,6 @@ loop.OTSdkDriver = (function() {
}));
this._notifyMetricsEvent("sdk.exception." + event.code);
break;
case OT.ExceptionCodes.UNABLE_TO_PUBLISH:
if (event.message === "GetUserMedia") {
// We free up the publisher here in case the store wants to try
// grabbing the media again.
if (this.publisher) {
this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated");
this.publisher.destroy();
delete this.publisher;
delete this._mockPublisherEl;
}
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
// No exception logging as this is a handled event.
} else {
// We need to log the message so that we can understand where the exception
// is coming from. Potentially a temporary addition.
this._notifyMetricsEvent("sdk.exception." + event.code + "." + event.message);
}
break;
case OT.ExceptionCodes.TERMS_OF_SERVICE_FAILURE:
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.TOS_FAILURE
@@ -1054,12 +1062,37 @@ loop.OTSdkDriver = (function() {
},
/**
* Called when a screenshare is denied. Notifies the other stores.
* Called when a screenshare is complete.
*
* @param {Error} error An OT error object, null if there was no error.
*/
_onScreenShareDenied: function() {
_onScreenSharePublishComplete: function(error) {
if (!error) {
// Nothing to do for the success case.
return;
}
// Free up publisher
this.screenshare.off("accessAllowed accessDenied streamCreated");
this.screenshare.destroy();
delete this.screenshare;
delete this._mockScreenSharePreviewEl;
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.INACTIVE
}));
this._notifyMetricsEvent("sdk.exception.screen." + error.code + "." + error.message);
},
/**
* Called when a screenshare is denied. Notifies the other stores.
*/
_onScreenSharePublishError: function() {
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.INACTIVE
}));
this.screenshare.off("accessAllowed accessDenied streamCreated");
this.screenshare.destroy();
delete this.screenshare;
delete this._mockScreenSharePreviewEl;
},

View File

@@ -25,7 +25,8 @@ loop.store.TextChatStore = (function() {
"dataChannelsAvailable",
"receivedTextChatMessage",
"sendTextChatMessage",
"updateRoomInfo"
"updateRoomInfo",
"updateRoomContext"
],
/**
@@ -134,7 +135,8 @@ loop.store.TextChatStore = (function() {
receivedTextChatMessage: function(actionData) {
// If we don't know how to deal with this content, then skip it
// as this version doesn't support it.
if (actionData.contentType !== CHAT_CONTENT_TYPES.TEXT) {
if (actionData.contentType !== CHAT_CONTENT_TYPES.TEXT &&
actionData.contentType !== CHAT_CONTENT_TYPES.CONTEXT_TILE) {
return;
}
@@ -187,6 +189,57 @@ loop.store.TextChatStore = (function() {
}
});
}
},
/**
* Handles receiving information about the room context due to a change of the tabs
*
* @param {sharedActions.updateRoomContext} actionData
*/
updateRoomContext: function(actionData) {
// Firstly, check if there is a previous context tile, if not, create it
var contextTile = null;
for (var i = this._storeState.messageList.length - 1; i >= 0; i--) {
if (this._storeState.messageList[i].contentType === CHAT_CONTENT_TYPES.CONTEXT_TILE) {
contextTile = this._storeState.messageList[i];
break;
}
}
if (!contextTile) {
this._appendContextTileMessage(actionData);
return;
}
var oldDomain = new URL(contextTile.extraData.newRoomURL).hostname;
var currentDomain = new URL(actionData.newRoomURL).hostname;
if (oldDomain === currentDomain) {
return;
}
this._appendContextTileMessage(actionData);
},
/**
* Appends a context tile message to the UI and sends it.
*
* @param {sharedActions.updateRoomContext} data
*/
_appendContextTileMessage: function(data) {
var msgData = {
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: data.newRoomDescription,
extraData: {
roomToken: data.roomToken,
newRoomThumbnail: data.newRoomThumbnail,
newRoomURL: data.newRoomURL
},
sentTimestamp: (new Date()).toISOString()
};
this.sendTextChatMessage(msgData);
}
});

View File

@@ -1,3 +1,5 @@
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/* 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/. */
@@ -5,7 +7,7 @@
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.chat = (function(mozL10n) {
loop.shared.views.chat = function (mozL10n) {
"use strict";
var sharedActions = loop.shared.actions;
@@ -17,15 +19,20 @@ loop.shared.views.chat = (function(mozL10n) {
/**
* Renders an individual entry for the text chat entries view.
*/
var TextChatEntry = React.createClass({displayName: "TextChatEntry",
var TextChatEntry = React.createClass({
displayName: "TextChatEntry",
mixins: [React.addons.PureRenderMixin],
propTypes: {
contentType: React.PropTypes.string.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
extraData: React.PropTypes.object,
message: React.PropTypes.string.isRequired,
showTimestamp: React.PropTypes.bool.isRequired,
timestamp: React.PropTypes.string.isRequired,
type: React.PropTypes.string.isRequired
type: React.PropTypes.string.isRequired,
useDesktopPaths: React.PropTypes.bool
},
/**
@@ -33,21 +40,18 @@ loop.shared.views.chat = (function(mozL10n) {
* (or L10N equivalent).
*
*/
_renderTimestamp: function() {
_renderTimestamp: function () {
var date = new Date(this.props.timestamp);
var language = mozL10n.language ? mozL10n.language.code
: mozL10n.getLanguage();
return (
React.createElement("span", {className: "text-chat-entry-timestamp"},
date.toLocaleTimeString(language,
{ hour: "numeric", minute: "numeric",
return React.createElement(
"span",
{ className: "text-chat-entry-timestamp" },
date.toLocaleTimeString(mozL10n.language.code, { hour: "numeric", minute: "numeric",
hour12: false })
)
);
},
render: function() {
render: function () {
var classes = classNames({
"text-chat-entry": true,
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
@@ -58,33 +62,54 @@ loop.shared.views.chat = (function(mozL10n) {
var optionalProps = {};
if (loop.shared.utils.isDesktop()) {
optionalProps.linkClickHandler = function(url) {
optionalProps.linkClickHandler = function (url) {
loop.request("OpenURL", url);
};
}
return (
React.createElement("div", {className: classes},
React.createElement(sharedViews.LinkifiedTextView, React.__spread({}, optionalProps,
{rawText: this.props.message})),
React.createElement("span", {className: "text-chat-arrow"}),
if (this.props.contentType === CHAT_CONTENT_TYPES.CONTEXT_TILE) {
return React.createElement(
"div",
{ className: classes },
React.createElement(sharedViews.ContextUrlView, {
allowClick: true,
description: this.props.message,
dispatcher: this.props.dispatcher,
thumbnail: this.props.extraData.newRoomThumbnail,
url: this.props.extraData.newRoomURL,
useDesktopPaths: this.props.useDesktopPaths }),
this.props.showTimestamp ? this._renderTimestamp() : null
);
}
return React.createElement(
"div",
{ className: classes },
React.createElement(sharedViews.LinkifiedTextView, _extends({}, optionalProps, {
rawText: this.props.message })),
React.createElement("span", { className: "text-chat-arrow" }),
this.props.showTimestamp ? this._renderTimestamp() : null
)
);
}
});
var TextChatRoomName = React.createClass({displayName: "TextChatRoomName",
var TextChatRoomName = React.createClass({
displayName: "TextChatRoomName",
mixins: [React.addons.PureRenderMixin],
propTypes: {
message: React.PropTypes.string.isRequired
},
render: function() {
return (
React.createElement("div", {className: "text-chat-header special room-name"},
React.createElement("p", null, mozL10n.get("rooms_welcome_title", { conversationName: this.props.message }))
render: function () {
return React.createElement(
"div",
{ className: "text-chat-header special room-name" },
React.createElement(
"p",
null,
mozL10n.get("rooms_welcome_title", { conversationName: this.props.message })
)
);
}
@@ -95,11 +120,10 @@ loop.shared.views.chat = (function(mozL10n) {
* TextChatView so that scrolling can be managed more efficiently - this
* component only updates when the message list is changed.
*/
var TextChatEntriesView = React.createClass({displayName: "TextChatEntriesView",
mixins: [
React.addons.PureRenderMixin,
sharedMixins.AudioMixin
],
var TextChatEntriesView = React.createClass({
displayName: "TextChatEntriesView",
mixins: [React.addons.PureRenderMixin, sharedMixins.AudioMixin],
statics: {
ONE_MINUTE: 60
@@ -112,31 +136,30 @@ loop.shared.views.chat = (function(mozL10n) {
useDesktopPaths: React.PropTypes.bool.isRequired
},
getInitialState: function() {
getInitialState: function () {
return {
receivedMessageCount: 0
};
},
_hasChatMessages: function() {
return this.props.messageList.some(function(message) {
_hasChatMessages: function () {
return this.props.messageList.some(function (message) {
return message.contentType === CHAT_CONTENT_TYPES.TEXT;
});
},
componentWillUpdate: function() {
componentWillUpdate: function () {
var node = this.getDOMNode();
if (!node) {
return;
}
// Scroll only if we're right at the bottom of the display, or if we've
// not had any chat messages so far.
this.shouldScroll = !this._hasChatMessages() ||
node.scrollHeight === node.scrollTop + node.clientHeight;
this.shouldScroll = !this._hasChatMessages() || node.scrollHeight === node.scrollTop + node.clientHeight;
},
componentWillReceiveProps: function(nextProps) {
var receivedMessageCount = nextProps.messageList.filter(function(message) {
componentWillReceiveProps: function (nextProps) {
var receivedMessageCount = nextProps.messageList.filter(function (message) {
return message.type === CHAT_MESSAGE_TYPES.RECEIVED;
}).length;
@@ -147,12 +170,12 @@ loop.shared.views.chat = (function(mozL10n) {
}
},
componentDidUpdate: function() {
componentDidUpdate: function () {
// Don't scroll if we haven't got any chat messages yet - e.g. for context
// display, we want to display starting at the top.
if (this.shouldScroll && this._hasChatMessages()) {
// This ensures the paint is complete.
window.requestAnimationFrame(function() {
window.requestAnimationFrame(function () {
try {
var node = this.getDOMNode();
node.scrollTop = node.scrollHeight - node.clientHeight;
@@ -163,7 +186,7 @@ loop.shared.views.chat = (function(mozL10n) {
}
},
render: function() {
render: function () {
/* Keep track of the last printed timestamp. */
var lastTimestamp = 0;
@@ -171,35 +194,36 @@ loop.shared.views.chat = (function(mozL10n) {
"text-chat-entries": true
});
return (
React.createElement("div", {className: entriesClasses},
React.createElement("div", {className: "text-chat-scroller"},
this.props.messageList.map(function(entry, i) {
return React.createElement(
"div",
{ className: entriesClasses },
React.createElement(
"div",
{ className: "text-chat-scroller" },
this.props.messageList.map(function (entry, i) {
if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) {
if (!this.props.showInitialContext) { return null; }
if (!this.props.showInitialContext) {
return null;
}
switch (entry.contentType) {
case CHAT_CONTENT_TYPES.ROOM_NAME:
return (
React.createElement(TextChatRoomName, {
return React.createElement(TextChatRoomName, {
key: i,
message: entry.message})
);
message: entry.message });
case CHAT_CONTENT_TYPES.CONTEXT:
return (
React.createElement("div", {className: "context-url-view-wrapper", key: i},
return React.createElement(
"div",
{ className: "context-url-view-wrapper", key: i },
React.createElement(sharedViews.ContextUrlView, {
allowClick: true,
description: entry.message,
dispatcher: this.props.dispatcher,
thumbnail: entry.extraData.thumbnail,
url: entry.extraData.location,
useDesktopPaths: this.props.useDesktopPaths})
)
useDesktopPaths: this.props.useDesktopPaths })
);
default:
console.error("Unsupported contentType",
entry.contentType);
console.error("Unsupported contentType", entry.contentType);
return null;
}
}
@@ -208,24 +232,22 @@ loop.shared.views.chat = (function(mozL10n) {
var timestamp = entry.receivedTimestamp || entry.sentTimestamp;
var timeDiff = this._isOneMinDelta(timestamp, lastTimestamp);
var shouldShowTimestamp = this._shouldShowTimestamp(i,
timeDiff);
var shouldShowTimestamp = this._shouldShowTimestamp(i, timeDiff);
if (shouldShowTimestamp) {
lastTimestamp = timestamp;
}
return (
React.createElement(TextChatEntry, {contentType: entry.contentType,
return React.createElement(TextChatEntry, { contentType: entry.contentType,
dispatcher: this.props.dispatcher,
extraData: entry.extraData,
key: i,
message: entry.message,
showTimestamp: shouldShowTimestamp,
timestamp: timestamp,
type: entry.type})
);
type: entry.type,
useDesktopPaths: this.props.useDesktopPaths });
}, this)
)
)
);
},
@@ -239,14 +261,13 @@ loop.shared.views.chat = (function(mozL10n) {
* @param {boolean} timeDiff If difference between consecutive messages is
* bigger than one minute.
*/
_shouldShowTimestamp: function(idx, timeDiff) {
_shouldShowTimestamp: function (idx, timeDiff) {
if (!idx) {
return true;
}
/* If consecutive messages are from different senders */
if (this.props.messageList[idx].type !==
this.props.messageList[idx - 1].type) {
if (this.props.messageList[idx].type !== this.props.messageList[idx - 1].type) {
return true;
}
@@ -262,7 +283,7 @@ loop.shared.views.chat = (function(mozL10n) {
* @param {string} currTime Timestamp of message yet to be rendered.
* @param {string} prevTime Last timestamp printed in the chat view.
*/
_isOneMinDelta: function(currTime, prevTime) {
_isOneMinDelta: function (currTime, prevTime) {
var date1 = new Date(currTime);
var date2 = new Date(prevTime);
var delta = date1 - date2;
@@ -283,11 +304,10 @@ loop.shared.views.chat = (function(mozL10n) {
* @property {Boolean} textChatEnabled Set to true to enable the box. If false, the
* text chat box won't be displayed.
*/
var TextChatInputView = React.createClass({displayName: "TextChatInputView",
mixins: [
React.addons.LinkedStateMixin,
React.addons.PureRenderMixin
],
var TextChatInputView = React.createClass({
displayName: "TextChatInputView",
mixins: [React.addons.LinkedStateMixin, React.addons.PureRenderMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@@ -295,7 +315,7 @@ loop.shared.views.chat = (function(mozL10n) {
textChatEnabled: React.PropTypes.bool.isRequired
},
getInitialState: function() {
getInitialState: function () {
return {
messageDetail: ""
};
@@ -307,7 +327,7 @@ loop.shared.views.chat = (function(mozL10n) {
*
* @param {Object} event The DOM event.
*/
handleKeyDown: function(event) {
handleKeyDown: function (event) {
if (event.which === 13) {
this.handleFormSubmit(event);
}
@@ -318,7 +338,7 @@ loop.shared.views.chat = (function(mozL10n) {
*
* @param {Object} event The DOM event.
*/
handleFormSubmit: function(event) {
handleFormSubmit: function (event) {
event.preventDefault();
// Don't send empty messages.
@@ -329,27 +349,29 @@ loop.shared.views.chat = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: this.state.messageDetail,
sentTimestamp: (new Date()).toISOString()
sentTimestamp: new Date().toISOString()
}));
// Reset the form to empty, ready for the next message.
this.setState({ messageDetail: "" });
},
render: function() {
render: function () {
if (!this.props.textChatEnabled) {
return null;
}
return (
React.createElement("div", {className: "text-chat-box"},
React.createElement("form", {onSubmit: this.handleFormSubmit},
return React.createElement(
"div",
{ className: "text-chat-box" },
React.createElement(
"form",
{ onSubmit: this.handleFormSubmit },
React.createElement("input", {
onKeyDown: this.handleKeyDown,
placeholder: this.props.showPlaceholder ? mozL10n.get("chat_textbox_placeholder") : "",
type: "text",
valueLink: this.linkState("messageDetail")})
)
valueLink: this.linkState("messageDetail") })
)
);
}
@@ -363,11 +385,10 @@ loop.shared.views.chat = (function(mozL10n) {
* @property {Boolean} showInitialContext Set to true to show the room name
* and initial context tile for linker clicker's special list items
*/
var TextChatView = React.createClass({displayName: "TextChatView",
mixins: [
React.addons.LinkedStateMixin,
loop.store.StoreMixin("textChatStore")
],
var TextChatView = React.createClass({
displayName: "TextChatView",
mixins: [React.addons.LinkedStateMixin, loop.store.StoreMixin("textChatStore")],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@@ -375,25 +396,23 @@ loop.shared.views.chat = (function(mozL10n) {
useDesktopPaths: React.PropTypes.bool.isRequired
},
getInitialState: function() {
getInitialState: function () {
return this.getStoreState();
},
render: function() {
render: function () {
var messageList = this.state.messageList;
// Filter out items not displayed when showing initial context.
// We do this here so that we can set the classes correctly on the view.
if (!this.props.showInitialContext) {
messageList = messageList.filter(function(item) {
return item.type !== CHAT_MESSAGE_TYPES.SPECIAL ||
(item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME &&
item.contentType !== CHAT_CONTENT_TYPES.CONTEXT);
messageList = messageList.filter(function (item) {
return item.type !== CHAT_MESSAGE_TYPES.SPECIAL || item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME && item.contentType !== CHAT_CONTENT_TYPES.CONTEXT;
});
}
// Only show the placeholder if we've sent messages.
var hasSentMessages = messageList.some(function(item) {
var hasSentMessages = messageList.some(function (item) {
return item.type === CHAT_MESSAGE_TYPES.SENT;
});
@@ -403,18 +422,18 @@ loop.shared.views.chat = (function(mozL10n) {
"text-chat-disabled": !this.state.textChatEnabled
});
return (
React.createElement("div", {className: textChatViewClasses},
return React.createElement(
"div",
{ className: textChatViewClasses },
React.createElement(TextChatEntriesView, {
dispatcher: this.props.dispatcher,
messageList: messageList,
showInitialContext: this.props.showInitialContext,
useDesktopPaths: this.props.useDesktopPaths}),
useDesktopPaths: this.props.useDesktopPaths }),
React.createElement(TextChatInputView, {
dispatcher: this.props.dispatcher,
showPlaceholder: !hasSentMessages,
textChatEnabled: this.state.textChatEnabled})
)
textChatEnabled: this.state.textChatEnabled })
);
}
});
@@ -424,4 +443,4 @@ loop.shared.views.chat = (function(mozL10n) {
TextChatEntry: TextChatEntry,
TextChatView: TextChatView
};
})(navigator.mozL10n || document.mozL10n);
}(navigator.mozL10n || document.mozL10n);

View File

@@ -105,7 +105,8 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
var CHAT_CONTENT_TYPES = {
CONTEXT: "chat-context",
TEXT: "chat-text",
ROOM_NAME: "room-name"
ROOM_NAME: "room-name",
CONTEXT_TILE: "context-tile"
};
/**
@@ -720,34 +721,6 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
return obj;
}
/**
* Truncate a string if it exceeds the length as defined in `maxLen`, which
* is defined as '72' characters by default. If the string needs trimming,
* it'll be suffixed with the unicode ellipsis char, \u2026.
*
* @param {String} str The string to truncate, if needed.
* @param {Number} maxLen Maximum number of characters that the string is
* allowed to contain. Optional, defaults to 72.
* @return {String} Truncated version of `str`.
*/
function truncate(str, maxLen) {
maxLen = maxLen || 72;
if (str.length > maxLen) {
var substring = str.substr(0, maxLen);
// XXX Due to the fact that we have two different l10n libraries.
var direction = mozL10n.getDirection ? mozL10n.getDirection() :
mozL10n.language.direction;
if (direction === "rtl") {
return "…" + substring;
}
return substring + "…";
}
return str;
}
/**
* Look up the DOM hierarchy for a node matching `selector`.
* If it is not found return the parent node, this is a sane default so
@@ -802,7 +775,6 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
strToUint8Array: strToUint8Array,
Uint8ArrayToStr: Uint8ArrayToStr,
objectDiff: objectDiff,
stripFalsyValues: stripFalsyValues,
truncate: truncate
stripFalsyValues: stripFalsyValues
};
}).call(inChrome ? this : loop.shared);

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -540,7 +540,7 @@ describe("loop.store.ActiveRoomStore", function() {
// easier.
sandbox.stub(loop.crypto, "decryptBytes", function() {
return {
then: function(resolve, reject) {
then: function(resolve) {
resolve(JSON.stringify(roomContext));
}
};
@@ -1552,10 +1552,15 @@ describe("loop.store.ActiveRoomStore", function() {
store.setStoreState({
roomState: ROOM_STATES.JOINED,
roomToken: "fakeToken",
sessionToken: "1627384950"
sessionToken: "1627384950",
participants: [{
displayName: "Owner",
owner: true
}, {
displayName: "Guest",
owner: false
}]
});
store.startBrowserShare(new sharedActions.StartBrowserShare());
});
afterEach(function() {
@@ -1563,6 +1568,7 @@ describe("loop.store.ActiveRoomStore", function() {
});
it("should set the state to 'pending'", function() {
store.startBrowserShare(new sharedActions.StartBrowserShare());
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.ScreenSharingState({
@@ -1571,10 +1577,12 @@ describe("loop.store.ActiveRoomStore", function() {
});
it("should add a browser sharing listener for tab sharing", function() {
store.startBrowserShare(new sharedActions.StartBrowserShare());
sinon.assert.calledOnce(requestStubs.AddBrowserSharingListener);
});
it("should invoke the SDK driver with the correct options for tab sharing", function() {
store.startBrowserShare(new sharedActions.StartBrowserShare());
sinon.assert.calledOnce(fakeSdkDriver.startScreenShare);
sinon.assert.calledWith(fakeSdkDriver.startScreenShare, {
videoSource: "browser",
@@ -1586,6 +1594,7 @@ describe("loop.store.ActiveRoomStore", function() {
});
it("should request the new metadata when the browser being shared change", function() {
store.startBrowserShare(new sharedActions.StartBrowserShare());
clock.tick(500);
sinon.assert.calledOnce(getSelectedTabMetadataStub);
sinon.assert.calledTwice(dispatcher.dispatch);
@@ -1599,6 +1608,7 @@ describe("loop.store.ActiveRoomStore", function() {
});
it("should process only one request", function() {
store.startBrowserShare(new sharedActions.StartBrowserShare());
// Simulates multiple requests.
LoopMochaUtils.publish("BrowserSwitch", 72);
LoopMochaUtils.publish("BrowserSwitch", 72);
@@ -1614,6 +1624,39 @@ describe("loop.store.ActiveRoomStore", function() {
roomToken: store.getStoreState().roomToken
}));
});
it("should not process a request without url", function() {
clock.tick(500);
getSelectedTabMetadataStub.returns({
title: "fakeTitle",
favicon: "fakeFavicon"
});
store.startBrowserShare(new sharedActions.StartBrowserShare());
sinon.assert.calledOnce(getSelectedTabMetadataStub);
sinon.assert.calledOnce(dispatcher.dispatch);
});
it("should not process a request if no-one is in the room", function() {
store.setStoreState({
roomState: ROOM_STATES.JOINED,
roomToken: "fakeToken",
sessionToken: "1627384950",
participants: [{
displayName: "Owner",
owner: true
}]
});
clock.tick(500);
getSelectedTabMetadataStub.returns({
title: "fakeTitle",
favicon: "fakeFavicon"
});
store.startBrowserShare(new sharedActions.StartBrowserShare());
sinon.assert.calledOnce(getSelectedTabMetadataStub);
sinon.assert.calledOnce(dispatcher.dispatch);
});
});
describe("Screen share Events", function() {

View File

@@ -4,26 +4,71 @@ import threading
import SimpleHTTPServer
import SocketServer
import BaseHTTPServer
import socket
import urllib
import urlparse
import os
DEBUG = False
# XXX Once we're on a branch with bug 993478 landed, we may want to get
# rid of this HTTP server and just use the built-in one from Marionette,
# since there will less code to maintain, and it will be faster. We'll
# need to consider whether this code wants to be shared with WebDriver tests
# for other browsers, though.
#
gCommonDir = None
# These redirects map the paths expected by the index.html files to the paths
# in mozilla-central. In the github repo, the unit tests are run entirely within
# karma, where the files are loaded directly. For mozilla-central we must map
# the files to the appropriate place.
REDIRECTIONS = {
"/test/vendor": "/chrome/content/shared/test/vendor",
"/shared/js": "/chrome/content/shared/js",
"/shared/test": "/chrome/content/shared/test",
"/shared/vendor": "/chrome/content/shared/vendor",
"/add-on/panels/vendor": "/chrome/content/panels/vendor",
"/add-on/panels/js": "/chrome/content/panels/js",
"/add-on/shared/js": "/chrome/content/shared/js",
"/add-on/shared/vendor": "/chrome/content/shared/vendor",
}
class ThreadingSimpleServer(SocketServer.ThreadingMixIn,
BaseHTTPServer.HTTPServer):
pass
class QuietHttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
class HttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def do_HEAD(s):
lastSlash = s.path.rfind("/")
path = s.path[:lastSlash]
if (path in REDIRECTIONS):
filename = s.path[lastSlash:]
s.send_response(301)
# Prefix the redirections with the common directory segment.
s.send_header("Location", "/" + gCommonDir + REDIRECTIONS.get(path, "/") + filename)
s.end_headers()
else:
SimpleHTTPServer.SimpleHTTPRequestHandler.do_HEAD(s)
def do_GET(s):
lastSlash = s.path.rfind("/")
path = s.path[:lastSlash]
if (path in REDIRECTIONS):
s.do_HEAD()
else:
SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(s)
def log_message(self, format, *args, **kwargs):
if DEBUG:
BaseHTTPServer.BaseHTTPRequestHandler.log_message(self, format, *args, **kwargs)
else:
pass
@@ -33,13 +78,8 @@ class BaseTestFrontendUnits(MarionetteTestCase):
def setUpClass(cls):
super(BaseTestFrontendUnits, cls).setUpClass()
if DEBUG:
handler = SimpleHTTPServer.SimpleHTTPRequestHandler
else:
handler = QuietHttpRequestHandler
# Port 0 means to select an arbitrary unused port
cls.server = ThreadingSimpleServer(('', 0), handler)
cls.server = ThreadingSimpleServer(('', 0), HttpRequestHandler)
cls.ip, cls.port = cls.server.server_address
cls.server_thread = threading.Thread(target=cls.server.serve_forever)
@@ -78,6 +118,8 @@ class BaseTestFrontendUnits(MarionetteTestCase):
# srcdir_path should be the directory relative to this file.
def set_server_prefix(self, srcdir_path):
global gCommonDir
# We may be run from a different path than topsrcdir, e.g. in the case
# of packaged tests. If so, then we have to work out the right directory
# for the local server.
@@ -90,6 +132,13 @@ class BaseTestFrontendUnits(MarionetteTestCase):
self.relPath = urllib.pathname2url(os.path.join(self.relPath, srcdir_path))
# This is the common directory segment, what you need to get from the
# common path to the relative path location. Used to get the redirects
# correct both locally and on the build systems.
# The .replace is to change windows path slashes to unix like ones for
# the url.
gCommonDir = os.path.normpath(self.relPath).replace("\\", "//")
# Finally join the relative path with the given src path
self.server_prefix = urlparse.urljoin("http://localhost:" + str(self.port),
self.relPath)
@@ -122,8 +171,9 @@ class BaseTestFrontendUnits(MarionetteTestCase):
# but not from marionette, uncomment the two lines below to break
# on failing tests, so that the browsers won't be torn down, and you
# can use the browser debugging facilities to see what's going on.
#from ipdb import set_trace
#set_trace()
#
# from ipdb import set_trace
# set_trace()
raise AssertionError(self.get_failure_details(page))

View File

@@ -6,7 +6,7 @@
<head>
<meta charset="utf-8">
<title>Loop shared mocha tests</title>
<link rel="stylesheet" media="all" href="vendor/mocha-2.2.5.css">
<link rel="stylesheet" media="all" href="/test/vendor/mocha.css">
</head>
<body>
<div id="mocha">
@@ -14,23 +14,26 @@
</div>
<div id="messages"></div>
<div id="fixtures"></div>
<script src="../../content/shared/vendor/lodash-3.9.3.js"></script>
<script src="../shared/loop_mocha_utils.js"></script>
<script src="/shared/vendor/lodash.js"></script>
<script src="loop_mocha_utils.js"></script>
<script>
LoopMochaUtils.trapErrors();
</script>
<!-- libs -->
<script src="../../content/shared/vendor/react-0.13.3.js"></script>
<script src="../../content/shared/vendor/classnames-2.2.0.js"></script>
<script src="../../content/shared/vendor/backbone-1.2.1.js"></script>
<script src="../../standalone/content/vendor/l10n-gaia-02ca67948fe8.js"></script>
<script src="/shared/vendor/react.js"></script>
<script src="/shared/vendor/classnames.js"></script>
<script src="/shared/vendor/backbone.js"></script>
<!-- Use desktop's l10n.js, since that's all we have when we export to
mozilla-central. -->
<script src="/add-on/panels/vendor/l10n.js"></script>
<!-- test dependencies -->
<script src="vendor/mocha-2.2.5.js"></script>
<script src="vendor/chai-3.0.0.js"></script>
<script src="vendor/chai-as-promised-5.1.0.js"></script>
<script src="vendor/sinon-1.16.1.js"></script>
<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;
@@ -38,22 +41,22 @@
</script>
<!-- App scripts -->
<script src="../../content/shared/js/loopapi-client.js"></script>
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/crypto.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../content/shared/js/store.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
<script src="../../content/shared/js/urlRegExps.js"></script>
<script src="../../content/shared/js/linkifiedTextView.js"></script>
<script src="/shared/js/loopapi-client.js"></script>
<script src="/shared/js/utils.js"></script>
<script src="/shared/js/models.js"></script>
<script src="/shared/js/mixins.js"></script>
<script src="/shared/js/crypto.js"></script>
<script src="/shared/js/validate.js"></script>
<script src="/shared/js/actions.js"></script>
<script src="/shared/js/dispatcher.js"></script>
<script src="/shared/js/otSdkDriver.js"></script>
<script src="/shared/js/store.js"></script>
<script src="/shared/js/activeRoomStore.js"></script>
<script src="/shared/js/views.js"></script>
<script src="/shared/js/textChatStore.js"></script>
<script src="/shared/js/textChatView.js"></script>
<script src="/shared/js/urlRegExps.js"></script>
<script src="/shared/js/linkifiedTextView.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>

View File

@@ -1,6 +1,8 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* exported LoopMochaUtils */
var LoopMochaUtils = (function(global, _) {
"use strict";
@@ -30,7 +32,7 @@ var LoopMochaUtils = (function(global, _) {
* @param {Function} asyncFn Function to invoke directly that contains async
* continuation(s).
*/
function syncThenable(asyncFn) {
function SyncThenable(asyncFn) {
var continuations = [];
var resolved = false;
var resolvedWith = null;
@@ -45,6 +47,12 @@ var LoopMochaUtils = (function(global, _) {
return this;
};
/**
* Used to resolve an object of type SyncThenable.
*
* @param {*} result The result to return.
* @return {SyncThenable} A resolved SyncThenable object.
*/
this.resolve = function(result) {
resolved = true;
resolvedWith = result;
@@ -69,8 +77,8 @@ var LoopMochaUtils = (function(global, _) {
asyncFn(this.resolve.bind(this), this.reject.bind(this));
}
syncThenable.all = function(promises) {
return new syncThenable(function(resolve) {
SyncThenable.all = function(promises) {
return new SyncThenable(function(resolve) {
var results = [];
promises.forEach(function(promise) {
@@ -83,15 +91,28 @@ var LoopMochaUtils = (function(global, _) {
});
};
/**
* This simulates the equivalent of Promise.resolve() - calling the
* resolve function on the raw object.
*
* @param {*} result The result to return.
* @return {SyncThenable} A resolved SyncThenable object.
*/
SyncThenable.resolve = function(result) {
return new SyncThenable(function(resolve) {
resolve(result);
});
};
/**
* Simple wrapper around `sinon.sandbox.create()` to also stub the native Promise
* object out with `syncThenable`.
* object out with `SyncThenable`.
*
* @return {Sandbox} A Sinon.JS sandbox object.
*/
function createSandbox() {
var sandbox = sinon.sandbox.create();
sandbox.stub(global, "Promise", syncThenable);
sandbox.stub(global, "Promise", SyncThenable);
return sandbox;
}
@@ -201,7 +222,8 @@ var LoopMochaUtils = (function(global, _) {
* to the listeners.
*/
function publish() {
var args = Array.slice(arguments);
// Convert to a proper array.
var args = Array.prototype.slice.call(arguments);
var name = args.shift();
gPushListenerCallbacks.forEach(function(cb) {
cb({ data: [name, args] });

View File

@@ -6,8 +6,6 @@ describe("loopapi-client", function() {
var expect = chai.expect;
var sandbox, clock, replyTimeoutMs;
var sharedMixins = loop.shared.mixins;
var TestUtils = React.addons.TestUtils;
beforeEach(function() {
sandbox = sinon.sandbox.create();

View File

@@ -6,7 +6,7 @@ describe("loop.shared.models", function() {
"use strict";
var expect = chai.expect;
var l10n = navigator.mozL10n;
var l10n = navigator.mozL10n || document.mozL10n;
var sharedModels = loop.shared.models;
var sandbox;

View File

@@ -12,8 +12,8 @@ describe("loop.OTSdkDriver", function() {
var CHAT_CONTENT_TYPES = loop.shared.utils.CHAT_CONTENT_TYPES;
var sandbox, constants;
var dispatcher, driver, requestStubs, publisher, sdk, session, sessionData, subscriber;
var publisherConfig, fakeEvent;
var dispatcher, driver, requestStubs, publisher, screenshare, sdk, session;
var sessionData, subscriber, publisherConfig, fakeEvent;
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
@@ -59,6 +59,8 @@ describe("loop.OTSdkDriver", function() {
}
}, Backbone.Events);
screenshare = publisher;
subscriber = _.extend({
_: {
getDataChannel: sinon.stub()
@@ -141,11 +143,14 @@ describe("loop.OTSdkDriver", function() {
});
describe("#setupStreamElements", function() {
it("should call initPublisher", function() {
beforeEach(function() {
sandbox.stub(publisher, "off");
driver.setupStreamElements(new sharedActions.SetupStreamElements({
publisherConfig: publisherConfig
}));
});
it("should call initPublisher", function() {
var expectedConfig = _.extend({
channels: {
text: {}
@@ -157,6 +162,45 @@ describe("loop.OTSdkDriver", function() {
sinon.match.instanceOf(HTMLDivElement),
expectedConfig);
});
it("should not do anything if publisher completed successfully", function() {
sdk.initPublisher.callArg(2);
sinon.assert.notCalled(publisher.off);
sinon.assert.notCalled(dispatcher.dispatch);
});
it("should clean up publisher if an error occurred", function() {
sdk.initPublisher.callArgWith(2, { message: "FAKE" });
sinon.assert.calledOnce(publisher.off);
sinon.assert.calledOnce(publisher.destroy);
expect(driver.publisher).to.equal(undefined);
expect(driver._mockPublisherEl).to.equal(undefined);
});
it("should dispatch ConnectionFailure if an error occurred", function() {
sdk.initPublisher.callArgWith(2, { message: "FAKE" });
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
});
it("should notify metrics if an error occurred", function() {
sdk.initPublisher.callArgWith(2, { code: 123, message: "FAKE" });
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionStatus({
event: "sdk.exception.123.FAKE",
state: "starting",
connections: 0,
sendStreams: 0,
recvStreams: 0
}));
});
});
describe("#setMute", function() {
@@ -190,28 +234,76 @@ describe("loop.OTSdkDriver", function() {
});
describe("#startScreenShare", function() {
beforeEach(function() {
sandbox.stub(driver, "_noteSharingState");
});
var options = {};
it("should initialize a publisher", function() {
// We're testing with `videoSource` set to 'browser', not 'window', as it
// has multiple options.
var options = {
beforeEach(function() {
sandbox.stub(screenshare, "off");
sandbox.stub(driver, "_noteSharingState");
options = {
videoSource: "browser",
constraints: {
browserWindow: 42,
scrollWithPage: true
}
};
driver.startScreenShare(options);
driver.startScreenShare(options);
});
it("should initialize a publisher", function() {
sinon.assert.calledOnce(sdk.initPublisher);
sinon.assert.calledWithMatch(sdk.initPublisher,
sinon.match.instanceOf(HTMLDivElement), options);
});
it("should log a telemetry action", function() {
sinon.assert.calledWithExactly(driver._noteSharingState, "browser", true);
});
it("should not do anything if publisher completed successfully", function() {
sdk.initPublisher.callArg(2);
sinon.assert.notCalled(screenshare.off);
sinon.assert.notCalled(dispatcher.dispatch);
});
it("should clean up publisher if an error occurred", function() {
sdk.initPublisher.callArgWith(2, { code: 123, message: "FAKE" });
sinon.assert.calledOnce(screenshare.off);
sinon.assert.calledOnce(screenshare.destroy);
expect(driver.screenshare).to.equal(undefined);
expect(driver._mockScreenSharePreviewEl).to.equal(undefined);
});
it("should dispatch ConnectionFailure if an error occurred", function() {
sdk.initPublisher.callArgWith(2, { code: 123, message: "FAKE" });
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.INACTIVE
}));
});
it("should notify metrics if an error occurred", function() {
sdk.initPublisher.callArgWith(2, { code: 123, message: "FAKE" });
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionStatus({
event: "sdk.exception.screen.123.FAKE",
state: "starting",
connections: 0,
sendStreams: 0,
recvStreams: 0
}));
});
});
describe("Screenshare Access Denied", function() {
beforeEach(function() {
sandbox.stub(screenshare, "off");
sandbox.stub(driver, "_noteSharingState");
var options = {
videoSource: "browser",
constraints: {
@@ -220,8 +312,23 @@ describe("loop.OTSdkDriver", function() {
}
};
driver.startScreenShare(options);
sdk.initPublisher.callArg(2);
driver.screenshare.trigger("accessDenied");
});
sinon.assert.calledWithExactly(driver._noteSharingState, "browser", true);
it("should clean up publisher", function() {
sinon.assert.calledOnce(screenshare.off);
sinon.assert.calledOnce(screenshare.destroy);
expect(driver.screenshare).to.equal(undefined);
expect(driver._mockScreenSharePreviewEl).to.equal(undefined);
});
it("should dispatch ConnectionFailure", function() {
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.INACTIVE
}));
});
});
@@ -1678,69 +1785,6 @@ describe("loop.OTSdkDriver", function() {
}));
});
describe("Unable to publish (not GetUserMedia)", function() {
it("should notify metrics", function() {
sdk.trigger("exception", {
code: OT.ExceptionCodes.UNABLE_TO_PUBLISH,
message: "Fake",
title: "Connect Failed"
});
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionStatus({
event: "sdk.exception." + OT.ExceptionCodes.UNABLE_TO_PUBLISH + ".Fake",
state: "starting",
connections: 0,
sendStreams: 0,
recvStreams: 0
}));
});
});
describe("Unable to publish (GetUserMedia)", function() {
it("should destroy the publisher", function() {
sdk.trigger("exception", {
code: OT.ExceptionCodes.UNABLE_TO_PUBLISH,
message: "GetUserMedia"
});
sinon.assert.calledOnce(publisher.destroy);
});
// XXX We should remove this when we stop being unable to publish as a
// workaround for knowing if the user has video as well as audio devices
// installed (bug 1138851).
it("should not notify metrics", function() {
sdk.trigger("exception", {
code: OT.ExceptionCodes.UNABLE_TO_PUBLISH,
message: "GetUserMedia"
});
sinon.assert.neverCalledWith(dispatcher.dispatch,
new sharedActions.ConnectionStatus({
event: "sdk.exception." + OT.ExceptionCodes.UNABLE_TO_PUBLISH,
state: "starting",
connections: 0,
sendStreams: 0,
recvStreams: 0
}));
});
it("should dispatch a ConnectionFailure action", function() {
sdk.trigger("exception", {
code: OT.ExceptionCodes.UNABLE_TO_PUBLISH,
message: "GetUserMedia"
});
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
});
});
describe("ToS Failure", function() {
it("should dispatch a ConnectionFailure action", function() {
sdk.trigger("exception", {

View File

@@ -10,7 +10,7 @@ class TestSharedUnits(BaseTestFrontendUnits):
def setUp(self):
super(TestSharedUnits, self).setUp()
self.set_server_prefix(".")
self.set_server_prefix("../../../../")
def test_units(self):
self.check_page("index.html")
self.check_page("chrome/content/shared/test/index.html")

View File

@@ -86,6 +86,34 @@ describe("loop.store.TextChatStore", function() {
}]);
});
it("should add the context tile to the list", function() {
store.receivedTextChatMessage({
type: CHAT_MESSAGE_TYPES.SENT,
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: "fake",
extraData: {
roomToken: "fakeRoomToken",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com"
},
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "1970-01-01T00:00:00.000Z"
});
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: "fake",
extraData: {
roomToken: "fakeRoomToken",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com"
},
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "1970-01-01T00:00:00.000Z"
}]);
});
it("should not add messages for unknown content types", function() {
store.receivedTextChatMessage({
contentType: "invalid type",
@@ -257,4 +285,53 @@ describe("loop.store.TextChatStore", function() {
sinon.assert.notCalled(window.dispatchEvent);
});
});
describe("#updateRoomContext", function() {
beforeEach(function() {
store.setStoreState({ messageList: [] });
store.updateRoomContext(new sharedActions.UpdateRoomContext({
newRoomDescription: "fake",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com",
roomToken: "fakeRoomToken"
}));
});
it("should add the room context to the list", function() {
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SENT,
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: "fake",
extraData: {
roomToken: "fakeRoomToken",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com"
},
sentTimestamp: "1970-01-01T00:00:00.000Z",
receivedTimestamp: undefined
}]);
});
it("should not add the room context if the last tile has the same domain", function() {
store.updateRoomContext(new sharedActions.UpdateRoomContext({
newRoomDescription: "fake",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com/test/same_domain",
roomToken: "fakeRoomToken"
}));
expect(store.getStoreState("messageList").length).eql(1);
});
it("should add the room context if the last tile has not the same domain", function() {
store.updateRoomContext(new sharedActions.UpdateRoomContext({
newRoomDescription: "fake",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.myfakeurl.com",
roomToken: "fakeRoomToken"
}));
expect(store.getStoreState("messageList").length).eql(2);
});
});
});

View File

@@ -6,18 +6,17 @@ describe("loop.shared.views.TextChatView", function() {
var expect = chai.expect;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var TestUtils = React.addons.TestUtils;
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
var CHAT_CONTENT_TYPES = loop.shared.utils.CHAT_CONTENT_TYPES;
var fixtures = document.querySelector("#fixtures");
var mozL10n = navigator.mozL10n || document.mozL10n;
var dispatcher, fakeSdkDriver, sandbox, store, fakeClock;
var dispatcher, fakeSdkDriver, originalLanguage, sandbox, store;
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
fakeClock = sandbox.useFakeTimers();
sandbox.useFakeTimers();
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
@@ -33,11 +32,23 @@ describe("loop.shared.views.TextChatView", function() {
loop.store.StoreMixin.register({
textChatStore: store
});
originalLanguage = mozL10n.language;
mozL10n.language = {
code: "en-US",
direction: "rtl"
};
sandbox.stub(mozL10n, "get", function(string) {
return string;
});
});
afterEach(function() {
sandbox.restore();
React.unmountComponentAtNode(fixtures);
mozL10n.language = originalLanguage;
});
describe("TextChatEntriesView", function() {
@@ -411,10 +422,6 @@ describe("loop.shared.views.TextChatView", function() {
// Fake server to catch all XHR requests.
fakeServer = sinon.fakeServer.create();
store.setStoreState({ textChatEnabled: true });
sandbox.stub(navigator.mozL10n, "get", function(string) {
return string;
});
});
afterEach(function() {

View File

@@ -6,6 +6,7 @@ describe("loop.shared.utils", function() {
"use strict";
var expect = chai.expect;
var mozL10n = navigator.mozL10n || document.mozL10n;
var sandbox;
var sharedUtils = loop.shared.utils;
@@ -333,7 +334,7 @@ describe("loop.shared.utils", function() {
beforeEach(function() {
// fake mozL10n
sandbox.stub(navigator.mozL10n, "get", function(id) {
sandbox.stub(mozL10n, "get", function(id) {
switch (id) {
case "share_email_subject7":
return "subject";
@@ -635,45 +636,4 @@ describe("loop.shared.utils", function() {
expect(obj).to.eql({ prop1: "null", prop3: true });
});
});
describe("#truncate", function() {
describe("ltr support", function() {
it("should default to 72 chars", function() {
var output = sharedUtils.truncate(new Array(75).join());
expect(output.length).to.eql(73); // 72 + …
});
it("should take a max size argument", function() {
var output = sharedUtils.truncate(new Array(73).join(), 20);
expect(output.length).to.eql(21); // 20 + …
});
});
describe("rtl support", function() {
var directionStub;
beforeEach(function() {
// XXX should use sandbox
// https://github.com/cjohansen/Sinon.JS/issues/781
directionStub = sinon.stub(navigator.mozL10n.language, "direction", {
get: function() {
return "rtl";
}
});
});
afterEach(function() {
directionStub.restore();
});
it("should support RTL", function() {
var output = sharedUtils.truncate(new Array(73).join(), 20);
expect(output.length).to.eql(21); // 20 + …
expect(output.substr(0, 1)).to.eql("…");
});
});
});
});

View File

@@ -0,0 +1,371 @@
(function () {
"use strict";
// Module systems magic dance.
/* istanbul ignore else */
if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
// NodeJS
module.exports = chaiAsPromised;
} else if (typeof define === "function" && define.amd) {
// AMD
define(function () {
return chaiAsPromised;
});
} else {
/*global self: false */
// Other environment (usually <script> tag): plug in to global chai instance directly.
chai.use(chaiAsPromised);
// Expose as a property of the global object so that consumers can configure the `transferPromiseness` property.
self.chaiAsPromised = chaiAsPromised;
}
chaiAsPromised.transferPromiseness = function (assertion, promise) {
assertion.then = promise.then.bind(promise);
};
chaiAsPromised.transformAsserterArgs = function (values) {
return values;
};
function chaiAsPromised(chai, utils) {
var Assertion = chai.Assertion;
var assert = chai.assert;
function isJQueryPromise(thenable) {
return typeof thenable.always === "function" &&
typeof thenable.done === "function" &&
typeof thenable.fail === "function" &&
typeof thenable.pipe === "function" &&
typeof thenable.progress === "function" &&
typeof thenable.state === "function";
}
function assertIsAboutPromise(assertion) {
if (typeof assertion._obj.then !== "function") {
throw new TypeError(utils.inspect(assertion._obj) + " is not a thenable.");
}
if (isJQueryPromise(assertion._obj)) {
throw new TypeError("Chai as Promised is incompatible with jQuery's thenables, sorry! Please use a " +
"Promises/A+ compatible library (see http://promisesaplus.com/).");
}
}
function method(name, asserter) {
utils.addMethod(Assertion.prototype, name, function () {
assertIsAboutPromise(this);
return asserter.apply(this, arguments);
});
}
function property(name, asserter) {
utils.addProperty(Assertion.prototype, name, function () {
assertIsAboutPromise(this);
return asserter.apply(this, arguments);
});
}
function doNotify(promise, done) {
promise.then(function () { done(); }, done);
}
// These are for clarity and to bypass Chai refusing to allow `undefined` as actual when used with `assert`.
function assertIfNegated(assertion, message, extra) {
assertion.assert(true, null, message, extra.expected, extra.actual);
}
function assertIfNotNegated(assertion, message, extra) {
assertion.assert(false, message, null, extra.expected, extra.actual);
}
function getBasePromise(assertion) {
// We need to chain subsequent asserters on top of ones in the chain already (consider
// `eventually.have.property("foo").that.equals("bar")`), only running them after the existing ones pass.
// So the first base-promise is `assertion._obj`, but after that we use the assertions themselves, i.e.
// previously derived promises, to chain off of.
return typeof assertion.then === "function" ? assertion : assertion._obj;
}
// Grab these first, before we modify `Assertion.prototype`.
var propertyNames = Object.getOwnPropertyNames(Assertion.prototype);
var propertyDescs = {};
propertyNames.forEach(function (name) {
propertyDescs[name] = Object.getOwnPropertyDescriptor(Assertion.prototype, name);
});
property("fulfilled", function () {
var that = this;
var derivedPromise = getBasePromise(that).then(
function (value) {
that._obj = value;
assertIfNegated(that,
"expected promise not to be fulfilled but it was fulfilled with #{act}",
{ actual: value });
return value;
},
function (reason) {
assertIfNotNegated(that,
"expected promise to be fulfilled but it was rejected with #{act}",
{ actual: reason });
}
);
chaiAsPromised.transferPromiseness(that, derivedPromise);
});
property("rejected", function () {
var that = this;
var derivedPromise = getBasePromise(that).then(
function (value) {
that._obj = value;
assertIfNotNegated(that,
"expected promise to be rejected but it was fulfilled with #{act}",
{ actual: value });
return value;
},
function (reason) {
assertIfNegated(that,
"expected promise not to be rejected but it was rejected with #{act}",
{ actual: reason });
// Return the reason, transforming this into a fulfillment, to allow further assertions, e.g.
// `promise.should.be.rejected.and.eventually.equal("reason")`.
return reason;
}
);
chaiAsPromised.transferPromiseness(that, derivedPromise);
});
method("rejectedWith", function (Constructor, message) {
var desiredReason = null;
var constructorName = null;
if (Constructor instanceof RegExp || typeof Constructor === "string") {
message = Constructor;
Constructor = null;
} else if (Constructor && Constructor instanceof Error) {
desiredReason = Constructor;
Constructor = null;
message = null;
} else if (typeof Constructor === "function") {
constructorName = (new Constructor()).name;
} else {
Constructor = null;
}
var that = this;
var derivedPromise = getBasePromise(that).then(
function (value) {
var assertionMessage = null;
var expected = null;
if (Constructor) {
assertionMessage = "expected promise to be rejected with #{exp} but it was fulfilled with " +
"#{act}";
expected = constructorName;
} else if (message) {
var verb = message instanceof RegExp ? "matching" : "including";
assertionMessage = "expected promise to be rejected with an error " + verb + " #{exp} but it " +
"was fulfilled with #{act}";
expected = message;
} else if (desiredReason) {
assertionMessage = "expected promise to be rejected with #{exp} but it was fulfilled with " +
"#{act}";
expected = desiredReason;
}
that._obj = value;
assertIfNotNegated(that, assertionMessage, { expected: expected, actual: value });
},
function (reason) {
if (Constructor) {
that.assert(reason instanceof Constructor,
"expected promise to be rejected with #{exp} but it was rejected with #{act}",
"expected promise not to be rejected with #{exp} but it was rejected with #{act}",
constructorName,
reason);
}
var reasonMessage = utils.type(reason) === "object" && "message" in reason ?
reason.message :
"" + reason;
if (message && reasonMessage !== null && reasonMessage !== undefined) {
if (message instanceof RegExp) {
that.assert(message.test(reasonMessage),
"expected promise to be rejected with an error matching #{exp} but got #{act}",
"expected promise not to be rejected with an error matching #{exp}",
message,
reasonMessage);
}
if (typeof message === "string") {
that.assert(reasonMessage.indexOf(message) !== -1,
"expected promise to be rejected with an error including #{exp} but got #{act}",
"expected promise not to be rejected with an error including #{exp}",
message,
reasonMessage);
}
}
if (desiredReason) {
that.assert(reason === desiredReason,
"expected promise to be rejected with #{exp} but it was rejected with #{act}",
"expected promise not to be rejected with #{exp}",
desiredReason,
reason);
}
}
);
chaiAsPromised.transferPromiseness(that, derivedPromise);
});
property("eventually", function () {
utils.flag(this, "eventually", true);
});
method("notify", function (done) {
doNotify(getBasePromise(this), done);
});
method("become", function (value) {
return this.eventually.deep.equal(value);
});
////////
// `eventually`
// We need to be careful not to trigger any getters, thus `Object.getOwnPropertyDescriptor` usage.
var methodNames = propertyNames.filter(function (name) {
return name !== "assert" && typeof propertyDescs[name].value === "function";
});
methodNames.forEach(function (methodName) {
Assertion.overwriteMethod(methodName, function (originalMethod) {
return function () {
doAsserterAsyncAndAddThen(originalMethod, this, arguments);
};
});
});
var getterNames = propertyNames.filter(function (name) {
return name !== "_obj" && typeof propertyDescs[name].get === "function";
});
getterNames.forEach(function (getterName) {
// Chainable methods are things like `an`, which can work both for `.should.be.an.instanceOf` and as
// `should.be.an("object")`. We need to handle those specially.
var isChainableMethod = Assertion.prototype.__methods.hasOwnProperty(getterName);
if (isChainableMethod) {
Assertion.overwriteChainableMethod(
getterName,
function (originalMethod) {
return function() {
doAsserterAsyncAndAddThen(originalMethod, this, arguments);
};
},
function (originalGetter) {
return function() {
doAsserterAsyncAndAddThen(originalGetter, this);
};
}
);
} else {
Assertion.overwriteProperty(getterName, function (originalGetter) {
return function () {
doAsserterAsyncAndAddThen(originalGetter, this);
};
});
}
});
function doAsserterAsyncAndAddThen(asserter, assertion, args) {
// Since we're intercepting all methods/properties, we need to just pass through if they don't want
// `eventually`, or if we've already fulfilled the promise (see below).
if (!utils.flag(assertion, "eventually")) {
return asserter.apply(assertion, args);
}
var derivedPromise = getBasePromise(assertion).then(function (value) {
// Set up the environment for the asserter to actually run: `_obj` should be the fulfillment value, and
// now that we have the value, we're no longer in "eventually" mode, so we won't run any of this code,
// just the base Chai code that we get to via the short-circuit above.
assertion._obj = value;
utils.flag(assertion, "eventually", false);
return args ? chaiAsPromised.transformAsserterArgs(args) : args;
}).then(function (args) {
asserter.apply(assertion, args);
// Because asserters, for example `property`, can change the value of `_obj` (i.e. change the "object"
// flag), we need to communicate this value change to subsequent chained asserters. Since we build a
// promise chain paralleling the asserter chain, we can use it to communicate such changes.
return assertion._obj;
});
chaiAsPromised.transferPromiseness(assertion, derivedPromise);
}
///////
// Now use the `Assertion` framework to build an `assert` interface.
var originalAssertMethods = Object.getOwnPropertyNames(assert).filter(function (propName) {
return typeof assert[propName] === "function";
});
assert.isFulfilled = function (promise, message) {
return (new Assertion(promise, message)).to.be.fulfilled;
};
assert.isRejected = function (promise, toTestAgainst, message) {
if (typeof toTestAgainst === "string") {
message = toTestAgainst;
toTestAgainst = undefined;
}
var assertion = (new Assertion(promise, message));
return toTestAgainst !== undefined ? assertion.to.be.rejectedWith(toTestAgainst) : assertion.to.be.rejected;
};
assert.becomes = function (promise, value, message) {
return assert.eventually.deepEqual(promise, value, message);
};
assert.doesNotBecome = function (promise, value, message) {
return assert.eventually.notDeepEqual(promise, value, message);
};
assert.eventually = {};
originalAssertMethods.forEach(function (assertMethodName) {
assert.eventually[assertMethodName] = function (promise) {
var otherArgs = Array.prototype.slice.call(arguments, 1);
var customRejectionHandler;
var message = arguments[assert[assertMethodName].length - 1];
if (typeof message === "string") {
customRejectionHandler = function (reason) {
throw new chai.AssertionError(message + "\n\nOriginal reason: " + utils.inspect(reason));
};
}
var returnedPromise = promise.then(
function (fulfillmentValue) {
return assert[assertMethodName].apply(assert, [fulfillmentValue].concat(otherArgs));
},
customRejectionHandler
);
returnedPromise.notify = function (done) {
doNotify(returnedPromise, done);
};
return returnedPromise;
};
});
}
}());

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,270 @@
@charset "utf-8";
body {
margin:0;
}
#mocha {
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: 60px 50px;
}
#mocha ul,
#mocha li {
margin: 0;
padding: 0;
}
#mocha ul {
list-style: none;
}
#mocha h1,
#mocha h2 {
margin: 0;
}
#mocha h1 {
margin-top: 15px;
font-size: 1em;
font-weight: 200;
}
#mocha h1 a {
text-decoration: none;
color: inherit;
}
#mocha h1 a:hover {
text-decoration: underline;
}
#mocha .suite .suite h1 {
margin-top: 0;
font-size: .8em;
}
#mocha .hidden {
display: none;
}
#mocha h2 {
font-size: 12px;
font-weight: normal;
cursor: pointer;
}
#mocha .suite {
margin-left: 15px;
}
#mocha .test {
margin-left: 15px;
overflow: hidden;
}
#mocha .test.pending:hover h2::after {
content: '(pending)';
font-family: arial, sans-serif;
}
#mocha .test.pass.medium .duration {
background: #c09853;
}
#mocha .test.pass.slow .duration {
background: #b94a48;
}
#mocha .test.pass::before {
content: '✓';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #00d6b2;
}
#mocha .test.pass .duration {
font-size: 9px;
margin-left: 5px;
padding: 2px 5px;
color: #fff;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
border-radius: 5px;
}
#mocha .test.pass.fast .duration {
display: none;
}
#mocha .test.pending {
color: #0b97c4;
}
#mocha .test.pending::before {
content: '◦';
color: #0b97c4;
}
#mocha .test.fail {
color: #c00;
}
#mocha .test.fail pre {
color: black;
}
#mocha .test.fail::before {
content: '✖';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #c00;
}
#mocha .test pre.error {
color: #c00;
max-height: 300px;
overflow: auto;
}
/**
* (1): approximate for browsers not supporting calc
* (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
* ^^ seriously
*/
#mocha .test pre {
display: block;
float: left;
clear: left;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid #eee;
max-width: 85%; /*(1)*/
max-width: calc(100% - 42px); /*(2)*/
word-wrap: break-word;
border-bottom-color: #ddd;
-webkit-border-radius: 3px;
-webkit-box-shadow: 0 1px 3px #eee;
-moz-border-radius: 3px;
-moz-box-shadow: 0 1px 3px #eee;
border-radius: 3px;
}
#mocha .test h2 {
position: relative;
}
#mocha .test a.replay {
position: absolute;
top: 3px;
right: 0;
text-decoration: none;
vertical-align: middle;
display: block;
width: 15px;
height: 15px;
line-height: 15px;
text-align: center;
background: #eee;
font-size: 15px;
-moz-border-radius: 15px;
border-radius: 15px;
-webkit-transition: opacity 200ms;
-moz-transition: opacity 200ms;
transition: opacity 200ms;
opacity: 0.3;
color: #888;
}
#mocha .test:hover a.replay {
opacity: 1;
}
#mocha-report.pass .test.fail {
display: none;
}
#mocha-report.fail .test.pass {
display: none;
}
#mocha-report.pending .test.pass,
#mocha-report.pending .test.fail {
display: none;
}
#mocha-report.pending .test.pass.pending {
display: block;
}
#mocha-error {
color: #c00;
font-size: 1.5em;
font-weight: 100;
letter-spacing: 1px;
}
#mocha-stats {
position: fixed;
top: 15px;
right: 10px;
font-size: 12px;
margin: 0;
color: #888;
z-index: 1;
}
#mocha-stats .progress {
float: right;
padding-top: 0;
}
#mocha-stats em {
color: black;
}
#mocha-stats a {
text-decoration: none;
color: inherit;
}
#mocha-stats a:hover {
border-bottom: 1px solid #eee;
}
#mocha-stats li {
display: inline-block;
margin: 0 5px;
list-style: none;
padding-top: 11px;
}
#mocha-stats canvas {
width: 40px;
height: 40px;
}
#mocha code .comment { color: #ddd; }
#mocha code .init { color: #2f6fad; }
#mocha code .string { color: #5890ad; }
#mocha code .keyword { color: #8a6343; }
#mocha code .number { color: #2f6fad; }
@media screen and (max-device-width: 480px) {
#mocha {
margin: 60px 0px;
}
#mocha #stats {
position: absolute;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -11,12 +11,11 @@ describe("loop.shared.views", function() {
var sharedActions = loop.shared.actions;
var sharedModels = loop.shared.models;
var sharedViews = loop.shared.views;
var getReactElementByClass = TestUtils.findRenderedDOMComponentWithClass;
var sandbox, fakeAudioXHR, dispatcher, OS, OSVersion;
var sandbox, clock, dispatcher, OS, OSVersion;
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
sandbox.useFakeTimers(); // exposes sandbox.clock as a fake timer
clock = sandbox.useFakeTimers(); // exposes sandbox.clock as a fake timer
sandbox.stub(l10n, "get", function(x) {
return "translated:" + x;
});
@@ -28,20 +27,6 @@ describe("loop.shared.views", function() {
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
fakeAudioXHR = {
open: sinon.spy(),
send: function() {},
abort: function() {},
getResponseHeader: function(header) {
if (header === "Content-Type") {
return "audio/ogg";
}
},
responseType: null,
response: new ArrayBuffer(10),
onload: null
};
OS = "mac";
OSVersion = { major: 10, minor: 10 };
sandbox.stub(loop.shared.utils, "getOS", function() {
@@ -108,177 +93,12 @@ describe("loop.shared.views", function() {
});
});
describe("SettingsControlButton", function() {
var requestStubs;
var support_url = "https://support.com";
beforeEach(function() {
LoopMochaUtils.stubLoopRequest(requestStubs = {
OpenURL: sandbox.stub(),
SetLoopPref: sandbox.stub(),
GetLoopPref: function(prefName) {
switch (prefName) {
case "support_url":
return support_url;
default:
return prefName;
}
}
});
sandbox.stub(console, "error");
});
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
React.createElement(sharedViews.SettingsControlButton, props));
}
it("should render a visible button", function() {
var settingsMenuItems = [{ id: "help" }];
var comp = mountTestComponent({ menuItems: settingsMenuItems });
var node = comp.getDOMNode().querySelector(".btn-settings");
expect(node.classList.contains("hide")).eql(false);
});
it("should not render anything", function() {
var comp = mountTestComponent();
expect(comp.getDOMNode()).to.eql(null);
});
it("should not show an undefined menu option", function() {
var settingsMenuItems = [
{ id: "not Defined" },
{ id: "help" }
];
var comp = mountTestComponent({ menuItems: settingsMenuItems });
var menuItems = comp.getDOMNode().querySelectorAll(".settings-menu > li");
expect(menuItems).to.have.length.of(1);
});
it("should log an error for an undefined menu option", function() {
var settingsMenuItems = [
{ id: "not Defined" },
{ id: "help" }
];
mountTestComponent({ menuItems: settingsMenuItems });
sinon.assert.calledOnce(console.error);
sinon.assert.calledWithMatch(console.error, "Invalid");
});
it("should not render anything if not exists any valid item to show", function() {
var settingsMenuItems = [
{ id: "not Defined" },
{ id: "another wrong menu item" }
];
var comp = mountTestComponent({ menuItems: settingsMenuItems });
expect(comp.getDOMNode()).to.eql(null);
});
it("should show the settings dropdown on click", function() {
var settingsMenuItems = [{ id: "help" }];
var comp = mountTestComponent({ menuItems: settingsMenuItems });
expect(comp.state.showMenu).eql(false);
TestUtils.Simulate.click(comp.getDOMNode().querySelector(".btn-settings"));
expect(comp.state.showMenu).eql(true);
});
it("should have a `menu-below` class on the dropdown when the prop is set.", function() {
var settingsMenuItems = [
{ id: "help" }
];
var comp = mountTestComponent({
menuBelow: true,
menuItems: settingsMenuItems
});
var menuItems = comp.getDOMNode().querySelector(".settings-menu");
expect(menuItems.classList.contains("menu-below")).eql(true);
});
it("should not have a `menu-below` class on the dropdown when the prop is not set.", function() {
var settingsMenuItems = [
{ id: "help" }
];
var comp = mountTestComponent({
menuItems: settingsMenuItems
});
var menuItems = comp.getDOMNode().querySelector(".settings-menu");
expect(menuItems.classList.contains("menu-below")).eql(false);
});
it("should show edit Context on menu when the option is enabled", function() {
var settingsMenuItems = [
{
id: "edit",
enabled: true,
visible: true,
onClick: function() {}
}
];
var comp = mountTestComponent({ menuItems: settingsMenuItems });
var node = comp.getDOMNode().querySelector(".settings-menu > li.entry-settings-edit");
expect(node.classList.contains("hide")).eql(false);
});
it("should hide edit Context on menu when the option is not visible", function() {
var settingsMenuItems = [
{
id: "edit",
enabled: false,
visible: false,
onClick: function() {}
}
];
var comp = mountTestComponent({ menuItems: settingsMenuItems });
var node = comp.getDOMNode().querySelector(".settings-menu > li.entry-settings-edit");
expect(node.classList.contains("hide")).eql(true);
});
it("should call onClick method when the edit context menu item is clicked", function() {
var onClickCalled = false;
var settingsMenuItems = [
{
id: "edit",
enabled: true,
visible: true,
onClick: sandbox.stub()
}
];
var comp = mountTestComponent({ menuItems: settingsMenuItems });
TestUtils.Simulate.click(comp.getDOMNode().querySelector(".settings-menu > li.entry-settings-edit"));
sinon.assert.calledOnce(settingsMenuItems[0].onClick);
});
it("should open a tab to the support url when the support menu item is clicked", function() {
var settingsMenuItems = [
{ id: "help" }
];
var comp = mountTestComponent({ menuItems: settingsMenuItems });
TestUtils.Simulate.click(comp.getDOMNode().querySelector(".settings-menu > li:last-child"));
sinon.assert.calledOnce(requestStubs.OpenURL);
sinon.assert.calledWithExactly(requestStubs.OpenURL, support_url);
});
});
describe("ConversationToolbar", function() {
var clock, hangup, publishStream;
var hangup, publishStream;
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
show: true
dispatcher: dispatcher
}, props || {});
return TestUtils.renderIntoDocument(
React.createElement(sharedViews.ConversationToolbar, props));
@@ -287,21 +107,6 @@ describe("loop.shared.views", function() {
beforeEach(function() {
hangup = sandbox.stub();
publishStream = sandbox.stub();
clock = sinon.useFakeTimers();
});
afterEach(function() {
clock.restore();
});
it("should not render the component when 'show' is false", function() {
var comp = mountTestComponent({
hangup: hangup,
publishStream: publishStream,
show: false
});
expect(comp.getDOMNode()).to.eql(null);
});
it("should start no idle", function() {
@@ -743,7 +548,7 @@ describe("loop.shared.views", function() {
expect(element.className).eql("no-video");
});
it("should display a video element if a source object is supplied", function() {
it("should display a video element if a source object is supplied", function(done) {
view = mountTestComponent({
displayAvatar: false,
mediaType: "local",
@@ -758,7 +563,18 @@ describe("loop.shared.views", function() {
expect(element).not.eql(null);
expect(element.className).eql("local-video");
// Google Chrome doesn't seem to set "muted" on the element at creation
// time, so we need to do the test as async.
clock.restore();
setTimeout(function() {
try {
expect(element.muted).eql(true);
done();
} catch (ex) {
done(ex);
}
}, 10);
});
// We test this function by itself, as otherwise we'd be into creating fake

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
/*!
Copyright (c) 2015 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/* global define */
(function () {
'use strict';
var hasOwn = {}.hasOwnProperty;
function classNames () {
var classes = '';
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (!arg) continue;
var argType = typeof arg;
if (argType === 'string' || argType === 'number') {
classes += ' ' + arg;
} else if (Array.isArray(arg)) {
classes += ' ' + classNames.apply(null, arg);
} else if (argType === 'object') {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes += ' ' + key;
}
}
}
}
return classes.substr(1);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// register as 'classnames', consistent with npm package name
define('classnames', function () {
return classNames;
});
} else {
window.classNames = classNames;
}
}());

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,246 @@
# 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/.
# Panel Strings
clientSuperShortname=Hello
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
loopMenuItem_label=Start a conversation…
loopMenuItem_accesskey=t
## LOCALIZATION_NOTE(sign_in_again_title_line_one, sign_in_again_title_line_two2):
## These are displayed together at the top of the panel when a user is needed to
## sign-in again. The emphesis is on the first line to get the user to sign-in again,
## and this is displayed in slightly larger font. Please arrange as necessary for
## your locale.
## {{clientShortname2}} will be replaced by the brand name for either string.
sign_in_again_title_line_one=Please sign in again
sign_in_again_title_line_two2=to continue using {{clientShortname2}}
sign_in_again_button=Sign In
## LOCALIZATION_NOTE(sign_in_again_use_as_guest_button2): {{clientSuperShortname}}
## will be replaced by the super short brandname.
sign_in_again_use_as_guest_button2=Use {{clientSuperShortname}} as a Guest
panel_browse_with_friend_button=Browse this page with a friend
panel_stop_sharing_tabs_button=Stop sharing your tabs
## LOCALIZATION_NOTE(first_time_experience_subheading2): Message inviting the
## user to create his or her first conversation.
first_time_experience_subheading2=Click the Hello button to browse Web pages with a friend.
## LOCALIZATION_NOTE(first_time_experience_content): Message describing
## ways to use Hello project.
first_time_experience_content=Use it to plan together, work together, laugh together.
first_time_experience_button_label2=See how it works
invite_header_text_bold=Invite someone to browse this page with you!
invite_header_text3=It takes two to use Firefox Hello, so send a friend a link to browse the Web with you!
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
## an iconic button for the invite view.
invite_copy_link_button=Copy Link
invite_copied_link_button=Copied!
invite_email_link_button=Email Link
invite_facebook_button3=Facebook
invite_your_link=Your link:
# Status text
display_name_guest=Guest
# Error bars
## LOCALIZATION NOTE(session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
## These may be displayed at the top of the panel.
session_expired_error_description=Session expired. All URLs you have previously created and shared will no longer work.
could_not_authenticate=Could Not Authenticate
password_changed_question=Did you change your password?
try_again_later=Please try again later
could_not_connect=Could Not Connect To The Server
check_internet_connection=Please check your internet connection
login_expired=Your Login Has Expired
service_not_available=Service Unavailable At This Time
problem_accessing_account=There Was A Problem Accessing Your Account
## LOCALIZATION NOTE(retry_button): Displayed when there is an error to retry
## the appropriate action.
retry_button=Retry
share_email_subject7=Your invitation to browse the Web together
## LOCALIZATION NOTE (share_email_body7): In this item, don't translate the
## part between {{..}} and leave the \n\n part alone
share_email_body7=A friend is waiting for you on Firefox Hello. Click the link to connect and browse the Web together: {{callUrl}}
## LOCALIZATION NOTE (share_email_body_context3): In this item, don't translate
## the part between {{..}} and leave the \n\n part alone.
share_email_body_context3=A friend is waiting for you on Firefox Hello. Click the link to connect and browse {{title}} together: {{callUrl}}
## LOCALIZATION NOTE (share_email_footer2): Common footer content for both email types
share_email_footer2=\n\n____________\nFirefox Hello lets you browse the Web with your friends. Use it when you want to get things done: plan together, work together, laugh together. Learn more at http://www.firefox.com/hello
## LOCALIZATION NOTE (share_tweeet): In this item, don't translate the part
## between {{..}}. Please keep the text below 117 characters to make sure it fits
## in a tweet.
share_tweet=Join me for a video conversation on {{clientShortname2}}!
share_add_service_button=Add a Service
## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copy Link
email_link_menuitem=Email Link
delete_conversation_menuitem2=Delete
panel_footer_signin_or_signup_link=Sign In or Sign Up
settings_menu_item_account=Account
settings_menu_item_settings=Settings
settings_menu_item_signout=Sign Out
settings_menu_item_signin=Sign In
settings_menu_item_turnnotificationson=Turn Notifications On
settings_menu_item_turnnotificationsoff=Turn Notifications Off
settings_menu_item_feedback=Submit Feedback
settings_menu_button_tooltip=Settings
# Conversation Window Strings
initiate_call_button_label2=Ready to start your conversation?
incoming_call_title2=Conversation Request
incoming_call_block_button=Block
hangup_button_title=Hang up
hangup_button_caption2=Exit
## LOCALIZATION NOTE (call_with_contact_title): The title displayed
## when calling a contact. Don't translate the part between {{..}} because
## this will be replaced by the contact's name.
call_with_contact_title=Conversation with {{contactName}}
# Outgoing conversation
outgoing_call_title=Start conversation?
initiate_audio_video_call_button2=Start
initiate_audio_video_call_tooltip2=Start a video conversation
initiate_audio_call_button2=Voice conversation
peer_ended_conversation2=The person you were calling has ended the conversation.
restart_call=Rejoin
## LOCALIZATION NOTE (contact_offline_title): Title which is displayed when the
## contact is offline.
contact_offline_title=This person is not online
## LOCALIZATION NOTE (call_timeout_notification_text): Title which is displayed
## when the call didn't go through.
call_timeout_notification_text=Your call did not go through.
## LOCALIZATION NOTE (cancel_button):
## This button is displayed when a call has failed.
cancel_button=Cancel
rejoin_button=Rejoin Conversation
cannot_start_call_session_not_ready=Can't start call, session is not ready.
network_disconnected=The network connection terminated abruptly.
connection_error_see_console_notification=Call failed; see console for details.
no_media_failure_message=No camera or microphone found.
ice_failure_message=Connection failed. Your firewall may be blocking calls.
## LOCALIZATION NOTE (legal_text_and_links3): In this item, don't translate the
## parts between {{..}} because these will be replaced with links with the labels
## from legal_text_tos and legal_text_privacy. clientShortname will be replaced
## by the brand name.
legal_text_and_links3=By using {{clientShortname}} you agree to the {{terms_of_use}} and {{privacy_notice}}.
legal_text_tos=Terms of Use
legal_text_privacy=Privacy Notice
## LOCALIZATION NOTE (powered_by_beforeLogo, powered_by_afterLogo):
## These 2 strings are displayed before and after a 'Telefonica'
## logo.
powered_by_beforeLogo=Powered by
powered_by_afterLogo=
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User
feedback_window_heading=How was your conversation?
feedback_request_button=Leave Feedback
tour_label=Tour
rooms_list_recently_browsed2=Recently browsed
rooms_list_currently_browsing2=Currently browsing
rooms_signout_alert=Open conversations will be closed
room_name_untitled_page=Untitled Page
## LOCALIZATION NOTE (door_hanger_return, door_hanger_prompt_name, door_hanger_button): Dialog message on leaving conversation
door_hanger_return=See you later! You can return to this shared session at any time through the Hello panel.
door_hanger_prompt_name=Would you like to give it a name that's easier to remember? Current name:
door_hanger_button=OK
# Infobar strings
infobar_screenshare_browser_message2=You are sharing your tabs. Any tab you click on can be seen by your friends
infobar_screenshare_paused_browser_message=Tab sharing is paused
infobar_button_gotit_label=Got it!
infobar_button_gotit_accesskey=G
infobar_button_pause_label=Pause
infobar_button_pause_accesskey=P
infobar_button_restart_label=Restart
infobar_button_restart_accesskey=e
infobar_button_resume_label=Resume
infobar_button_resume_accesskey=R
infobar_button_stop_label=Stop
infobar_button_stop_accesskey=S
infobar_menuitem_dontshowagain_label=Don't show this again
infobar_menuitem_dontshowagain_accesskey=D
# E10s not supported strings
e10s_not_supported_button_label=Launch New Window
e10s_not_supported_subheading={{brandShortname}} doesn't work in a multi-process window.
# 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/.
## LOCALIZATION NOTE: In this file, don't translate the part between {{..}}
# Text chat strings
chat_textbox_placeholder=Type here…
## LOCALIZATION NOTE(clientShortname2): This should not be localized and
## should remain "Firefox Hello" for all locales.
clientShortname2=Firefox Hello
conversation_has_ended=Your conversation has ended.
generic_failure_message=We're having technical difficulties…
generic_failure_no_reason2=Would you like to try again?
help_label=Help
mute_local_audio_button_title=Mute your audio
unmute_local_audio_button_title=Unmute your audio
mute_local_video_button_title2=Disable video
unmute_local_video_button_title2=Enable video
## LOCALIZATION NOTE (retry_call_button):
## This button is displayed when a call has failed.
retry_call_button=Retry
rooms_leave_button_label=Leave
rooms_panel_title=Choose a conversation or start a new one
rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
rooms_room_full_label=There are already two people in this conversation.
rooms_room_join_label=Join the conversation
rooms_room_joined_label=Someone has joined the conversation!
self_view_hidden_message=Self-view hidden but still being sent; resize window to show
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.
tos_failure_message={{clientShortname}} is not available in your country.

View File

@@ -1,5 +1,4 @@
{
"extends": "../../.eslintrc-gecko",
"globals": {
// General test items.
"add_task": false,

View File

@@ -3,7 +3,7 @@ support-files =
head.js
loop_fxa.sjs
test_loopLinkClicker_channel.html
../../../../base/content/test/general/browser_fxa_oauth_with_keys.html
../../../../../base/content/test/general/browser_fxa_oauth_with_keys.html
[browser_fxa_login.js]
[browser_loop_fxa_server.js]

View File

@@ -12,7 +12,7 @@ var { WebChannel } = Cu.import("resource://gre/modules/WebChannel.jsm", {});
var { Chat } = Cu.import("resource:///modules/Chat.jsm", {});
const TEST_URI =
"example.com/browser/browser/extensions/loop/test/mochitest/test_loopLinkClicker_channel.html";
"example.com/browser/browser/extensions/loop/chrome/test/mochitest/test_loopLinkClicker_channel.html";
const TEST_URI_GOOD = Services.io.newURI("https://" + TEST_URI, null, null);
const TEST_URI_BAD = Services.io.newURI("http://" + TEST_URI, null, null);
@@ -54,7 +54,7 @@ var gBadBackChannel;
// Loads the specified URI in a new tab and waits for it to send us data on our
// test web-channel and resolves with that data.
function promiseNewChannelResponse(uri, channel, hash) {
let waitForChannelPromise = new Promise((resolve, reject) => {
let waitForChannelPromise = new Promise((resolve) => {
if (channel.receivedData) {
let data = channel.receivedData;
channel.receivedData = null;

View File

@@ -52,7 +52,7 @@ add_task(function* setup() {
}
});
registerCleanupFunction(function* () {
info("cleanup time");
info("cleanup specific to setup test");
yield promiseDeletedOAuthParams(BASE_URL);
Services.prefs.clearUserPref("loop.gettingStarted.latestFTUVersion");
MozLoopServiceInternal.mocks.pushHandler = undefined;
@@ -218,7 +218,7 @@ add_task(function* registrationWithInvalidState() {
Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
let tokenPromise = MozLoopServiceInternal.promiseFxAOAuthToken("code1", "state");
yield tokenPromise.then(body => {
yield tokenPromise.then(() => {
ok(false, "Promise should have rejected");
},
error => {
@@ -241,7 +241,7 @@ add_task(function* registrationWith401() {
yield promiseOAuthParamsSetup(BASE_URL, params);
let tokenPromise = MozLoopServiceInternal.promiseFxAOAuthToken("code1", "state");
yield tokenPromise.then(body => {
yield tokenPromise.then(() => {
ok(false, "Promise should have rejected");
},
error => {
@@ -357,7 +357,7 @@ add_task(function* loginWithParams401() {
yield MozLoopService.promiseRegisteredWithServers();
let loginPromise = MozLoopService.logInToFxA();
yield loginPromise.then(tokenData => {
yield loginPromise.then(() => {
ok(false, "Promise should have rejected");
},
error => {
@@ -379,7 +379,7 @@ add_task(function* logoutWithIncorrectPushURL() {
is(registrationResponse.response.simplePushURLs.rooms, pushURL, "Check registered push URL");
MozLoopServiceInternal.pushURLs.get(LOOP_SESSION_TYPE.FXA).rooms = "http://www.example.com/invalid";
let caught = false;
yield MozLoopService.logOutFromFxA().catch((error) => {
yield MozLoopService.logOutFromFxA().catch(() => {
caught = true;
});
ok(caught, "Should have caught an error logging out with a mismatched push URL");
@@ -418,7 +418,7 @@ add_task(function* loginWithRegistration401() {
yield promiseOAuthParamsSetup(BASE_URL, params);
let loginPromise = MozLoopService.logInToFxA();
yield loginPromise.then(tokenData => {
yield loginPromise.then(() => {
ok(false, "Promise should have rejected");
},
error => {
@@ -437,6 +437,17 @@ add_task(function* openFxASettings() {
// blank tab.
gBrowser.selectedTab = gBrowser.addTab(BASE_URL);
let fxASampleToken = {
token_type: "bearer",
access_token: "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",
scope: "profile"
};
let fxASampleProfile = {
email: "test@example.com",
uid: "abcd1234"
};
let params = {
client_id: "client_id",
content_uri: BASE_URL + "/content",
@@ -445,9 +456,20 @@ add_task(function* openFxASettings() {
state: "state",
test_error: "token_401"
};
Services.prefs.setCharPref("loop.fxa_oauth.profile", JSON.stringify(fxASampleProfile));
Services.prefs.setCharPref("loop.fxa_oauth.tokendata", JSON.stringify(fxASampleToken));
yield promiseOAuthParamsSetup(BASE_URL, params);
yield new Promise((resolve, reject) => {
registerCleanupFunction(function* () {
info("cleanup specific to openFxASettings test");
yield promiseDeletedOAuthParams(BASE_URL);
Services.prefs.clearUserPref("loop.fxa_oauth.profile");
Services.prefs.clearUserPref("loop.fxa_oauth.tokendata");
});
yield new Promise((resolve) => {
let progressListener = {
onLocationChange: function onLocationChange(aBrowser) {
if (aBrowser.currentURI.spec == BASE_URL) {
@@ -456,7 +478,7 @@ add_task(function* openFxASettings() {
}
gBrowser.removeTabsProgressListener(progressListener);
let contentURI = Services.io.newURI(params.content_uri, null, null);
is(aBrowser.currentURI.spec, Services.io.newURI("/settings", null, contentURI).spec,
is(aBrowser.currentURI.spec, Services.io.newURI("/settings?uid=abcd1234", null, contentURI).spec,
"Check settings tab URL");
resolve();
}

View File

@@ -7,7 +7,7 @@
"use strict";
const BASE_URL = "http://mochi.test:8888/browser/browser/extensions/loop/test/mochitest/loop_fxa.sjs?";
const BASE_URL = "http://mochi.test:8888/browser/browser/extensions/loop/chrome/test/mochitest/loop_fxa.sjs?";
registerCleanupFunction(function* () {
yield promiseDeletedOAuthParams(BASE_URL);

View File

@@ -130,7 +130,6 @@ add_task(function* test_multipleListener() {
});
add_task(function* test_infoBar() {
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const kBrowserSharingNotificationId = "loop-sharing-notification";
const kPrefBrowserSharingInfoBar = "loop.browserSharing.showInfoBar";

View File

@@ -8,7 +8,6 @@
"use strict";
Components.utils.import("resource://gre/modules/Promise.jsm", this);
const { LoopRoomsInternal } = Components.utils.import("chrome://loop/content/modules/LoopRooms.jsm", {});
Services.prefs.setIntPref("loop.gettingStarted.latestFTUVersion", 1);
const fxASampleToken = {

View File

@@ -3,6 +3,11 @@
"use strict";
/* exported HAWK_TOKEN_LENGTH, LoopRooms, promiseWaitForCondition,
loadLoopPanel, promiseOAuthParamsSetup, resetFxA, checkLoggedOutState,
promiseDeletedOAuthParams, promiseOAuthGetRegistration,
getLoopString, mockPushHandler, channelID, mockDb, LoopAPI */
const HAWK_TOKEN_LENGTH = 64;
const {
LOOP_SESSION_TYPE,
@@ -18,7 +23,7 @@ const { LoopRooms } = Cu.import("chrome://loop/content/modules/LoopRooms.jsm", {
const WAS_OFFLINE = Services.io.offline;
function promisePanelLoaded() {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
let loopPanel = document.getElementById("loop-notification-panel");
let btn = document.getElementById("loop-button");
@@ -43,7 +48,7 @@ function promisePanelLoaded() {
iframe.contentDocument.readyState == "complete") {
resolve();
} else {
iframe.addEventListener("load", function panelOnLoad(e) {
iframe.addEventListener("load", function panelOnLoad() {
iframe.removeEventListener("load", panelOnLoad, true);
// We do this in an execute soon to allow any other event listeners to
// be handled, just in case.
@@ -89,7 +94,7 @@ function waitForCondition(condition, nextTest, errorMsg) {
}
function promiseWaitForCondition(aConditionFn) {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
waitForCondition(aConditionFn, resolve, "Condition didn't pass.");
});
}
@@ -175,7 +180,7 @@ function promiseDeletedOAuthParams(baseURL) {
}
function promiseObserverNotified(aTopic, aExpectedData = null) {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
Services.obs.addObserver(function onNotification(aSubject, topic, aData) {
Services.obs.removeObserver(onNotification, topic);
is(aData, aExpectedData, "observer data should match expected data");
@@ -235,7 +240,7 @@ var mockPushHandler = {
setTimeout(registerCallback(this.registrationResult, this.registeredChannels[channelId], channelId), 0);
},
unregister: function(channelID) {
unregister: function() {
return;
},

View File

@@ -1,5 +1,4 @@
{
"extends": "../../.eslintrc-gecko",
"globals": {
// General xpcshell-test functions
"HttpServer": false,

View File

@@ -3,6 +3,11 @@
"use strict";
/* exported Cr, LoopRoomsInternal, timerHandlers, kMockWebSocketChannelName,
kWebSocketChannelContractID, kServerPushUrl, kLoopServerUrl,
setupFakeLoopServer, setupFakeFxAUserProfile, waitForCondition,
getLoopString, extend */
var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
// Initialize this before the imports, as some of them need it.
@@ -159,7 +164,7 @@ var mockPushHandler = {
registerCallback(this.registrationResult, this.registrationPushURL, channelId);
},
unregister: function(channelID) {
unregister: function() {
return;
},
@@ -183,7 +188,7 @@ MockWebSocketChannel.prototype = {
initRegStatus: 0,
defaultMsgHandler: function(msg) {
defaultMsgHandler: function() {
// Treat as a ping
this.listener.onMessageAvailable(this.context,
JSON.stringify({}));
@@ -231,7 +236,7 @@ MockWebSocketChannel.prototype = {
}
},
close: function(aCode, aReason) {
close: function(aCode) {
this.stop(aCode);
},

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
const { LoopAPI } = Cu.import("chrome://loop/content/modules/MozLoopAPI.jsm", {});
const [LoopAPIInternal] = LoopAPI.inspect();

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
var dummyCallback = () => {};
var mockWebSocket = new MockWebSocketChannel();
var pushServerRequestCount = 0;

View File

@@ -4,6 +4,8 @@
"use strict";
/* exported run_test */
Cu.import("resource://services-common/utils.js");
Cu.import("chrome://loop/content/modules/LoopRooms.jsm");
Cu.import("resource:///modules/Chat.jsm");
@@ -169,12 +171,6 @@ const kCreateRoomProps = {
maxSize: 2
};
const kCreateRoomUnencryptedProps = {
roomName: "UX Discussion",
roomOwner: "Alexis",
maxSize: 2
};
const kCreateRoomData = {
roomToken: "_nxD4V4FflQ",
roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
@@ -261,7 +257,7 @@ const onRoomLeft = function(e, room, participant) {
}
};
const onRefresh = function(e) {
const onRefresh = function() {
Assert.ok(gExpectedRefresh, "A refresh event should've been expected");
gExpectedRefresh = false;
};

View File

@@ -4,6 +4,8 @@
"use strict";
/* exported run_test */
timerHandlers.startTimer = callback => callback();
Cu.import("resource://services-common/utils.js");
@@ -184,7 +186,7 @@ add_task(function* setup_server() {
// Test if getting rooms saves unknown keys correctly.
add_task(function* test_get_rooms_saves_unknown_keys() {
let rooms = yield LoopRooms.promise("getAll");
yield LoopRooms.promise("getAll");
// Check that we've saved the encryption keys correctly.
let roomsCache = yield readRoomsCache();

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
Cu.import("resource://services-common/utils.js");
Cu.import("chrome://loop/content/modules/LoopRooms.jsm");
Cu.import("resource:///modules/Chat.jsm");

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
Cu.import("resource://services-common/utils.js");
Cu.import("resource:///modules/Chat.jsm");
Cu.import("resource://gre/modules/Promise.jsm");

View File

@@ -4,10 +4,11 @@
"use strict";
/* exported run_test */
Cu.import("resource://services-common/utils.js");
const loopCrypto = Cu.import("chrome://loop/content/shared/js/crypto.js", {}).LoopCrypto;
const { LOOP_ROOMS_CACHE_FILENAME } = Cu.import("chrome://loop/content/modules/LoopRoomsCache.jsm", {});
var gTimerArgs = [];

View File

@@ -4,6 +4,8 @@
"use strict";
/* exported run_test */
XPCOMUtils.defineLazyModuleGetter(this, "Chat",
"resource:///modules/Chat.jsm");
var openChatOrig = Chat.open;

View File

@@ -10,6 +10,8 @@
"use strict";
/* exported run_test */
const { INVALID_AUTH_TOKEN } = Cu.import("chrome://loop/content/modules/MozLoopService.jsm");
/**
@@ -61,7 +63,7 @@ add_task(function* guest_401() {
Services.prefs.setCharPref("loop.hawk-session-token.fxa", "fxa");
yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/401", "POST").then(
() => Assert.ok(false, "Should have rejected"),
(error) => {
() => {
Assert.strictEqual(Services.prefs.getPrefType("loop.hawk-session-token"),
Services.prefs.PREF_INVALID,
"Guest session token should have been cleared");
@@ -85,7 +87,7 @@ add_task(function* fxa_401() {
Services.prefs.setCharPref("loop.hawk-session-token.fxa", "fxa");
yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.FXA, "/401", "POST").then(
() => Assert.ok(false, "Should have rejected"),
(error) => {
() => {
Assert.strictEqual(Services.prefs.getCharPref("loop.hawk-session-token"),
"guest",
"Guest session token should NOT have been cleared");

View File

@@ -7,6 +7,8 @@
"use strict";
/* exported run_test */
Cu.import("resource://services-common/utils.js");
add_task(function* request_with_unicode() {

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
var startTimerCalled = false;
/**

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
function test_locale() {
// Set the pref to something controlled.
Services.prefs.setCharPref("general.useragent.locale", "ab-CD");

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
var fakeCharPrefName = "color";
var fakeBoolPrefName = "boolean";
var fakePrefValue = "green";

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
Cu.import("resource://services-common/utils.js");
/**

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://services-common/utils.js");

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
const FAKE_FXA_TOKEN_DATA = JSON.stringify({
"token_type": "bearer",
"access_token": "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
const LOOP_HAWK_PREF = "loop.hawk-session-token";
const fakeSessionToken1 = "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1751";
const fakeSessionToken2 = "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1750";

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
/**
* Test that things behave reasonably when a reasonable Hawk-Session-Token
* header is returned with the registration response.

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
add_test(function test_registration_uses_hawk_session_token() {
Services.prefs.setCharPref("loop.hawk-session-token",
"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1750");

View File

@@ -3,6 +3,8 @@
"use strict";
/* exported run_test */
// XXX should report error if Hawk-Session-Token is lexically invalid
// (not a string of 64 hex digits) to help resist other possible injection
// attacks. For now, however, we're just checking if it's the right length.

View File

@@ -32,7 +32,7 @@
</em:targetApplication>
<!-- Front End MetaData -->
<em:name>Firefox Hello</em:name>
<em:name>Firefox Hello Beta</em:name>
<em:description>Web sharing for Firefox</em:description>
<em:creator>Mozilla</em:creator>
</Description>

View File

@@ -26,139 +26,20 @@
% override chrome://loop/skin/toolbar@2x.png chrome://loop/skin/toolbar-aero@2x.png os=WINNT osversion=6.1
% override chrome://loop/skin/toolbar@2x.png chrome://loop/skin/toolbar-win10@2x.png os=WINNT osversion=6.2
% override chrome://loop/skin/toolbar@2x.png chrome://loop/skin/toolbar-win10@2x.png os=WINNT osversion=6.3
skin/ (skin/*)
content/modules/ (content/modules/*)
* content/preferences/prefs.js (content/preferences/prefs.js)
# Desktop html files
content/panels/conversation.html (content/panels/conversation.html)
content/panels/panel.html (content/panels/panel.html)
# Desktop vendor (see bottom of this file for TokBox sdk assets)
content/panels/vendor/l10n.js (content/panels/vendor/l10n.js)
# Desktop script
content/panels/js/conversation.js (content/panels/js/conversation.js)
content/panels/js/conversationAppStore.js (content/panels/js/conversationAppStore.js)
content/panels/js/otconfig.js (content/panels/js/otconfig.js)
content/panels/js/panel.js (content/panels/js/panel.js)
content/panels/js/roomStore.js (content/panels/js/roomStore.js)
content/panels/js/roomViews.js (content/panels/js/roomViews.js)
content/panels/js/feedbackViews.js (content/panels/js/feedbackViews.js)
# Desktop styles
content/panels/css/panel.css (content/panels/css/panel.css)
# Shared styles
content/shared/css/reset.css (content/shared/css/reset.css)
content/shared/css/common.css (content/shared/css/common.css)
content/shared/css/conversation.css (content/shared/css/conversation.css)
# Shared images
content/shared/img/helloicon.svg (content/shared/img/helloicon.svg)
content/shared/img/icon_32.png (content/shared/img/icon_32.png)
content/shared/img/icon_64.png (content/shared/img/icon_64.png)
content/shared/img/spinner.svg (content/shared/img/spinner.svg)
# XXX could get rid of the png spinner usages and replace them with the svg
# one?
content/shared/img/spinner.png (content/shared/img/spinner.png)
content/shared/img/spinner@2x.png (content/shared/img/spinner@2x.png)
content/shared/img/sad_hello_icon_64x64.svg (content/shared/img/sad_hello_icon_64x64.svg)
content/shared/img/chatbubble-arrow-left.svg (content/shared/img/chatbubble-arrow-left.svg)
content/shared/img/chatbubble-arrow-right.svg (content/shared/img/chatbubble-arrow-right.svg)
content/shared/img/facemute-14x14.png (content/shared/img/facemute-14x14.png)
content/shared/img/facemute-14x14@2x.png (content/shared/img/facemute-14x14@2x.png)
content/shared/img/hangup-inverse-14x14.png (content/shared/img/hangup-inverse-14x14.png)
content/shared/img/hangup-inverse-14x14@2x.png (content/shared/img/hangup-inverse-14x14@2x.png)
content/shared/img/mute-inverse-14x14.png (content/shared/img/mute-inverse-14x14.png)
content/shared/img/mute-inverse-14x14@2x.png (content/shared/img/mute-inverse-14x14@2x.png)
content/shared/img/glyph-email-16x16.svg (content/shared/img/glyph-email-16x16.svg)
content/shared/img/glyph-facebook-16x16.svg (content/shared/img/glyph-facebook-16x16.svg)
content/shared/img/glyph-help-16x16.svg (content/shared/img/glyph-help-16x16.svg)
content/shared/img/glyph-link-16x16.svg (content/shared/img/glyph-link-16x16.svg)
content/shared/img/glyph-user-16x16.svg (content/shared/img/glyph-user-16x16.svg)
content/shared/img/exit.svg (content/shared/img/exit.svg)
content/shared/img/audio.svg (content/shared/img/audio.svg)
content/shared/img/audio-hover.svg (content/shared/img/audio-hover.svg)
content/shared/img/audio-mute.svg (content/shared/img/audio-mute.svg)
content/shared/img/audio-mute-hover.svg (content/shared/img/audio-mute-hover.svg)
content/shared/img/video.svg (content/shared/img/video.svg)
content/shared/img/video-hover.svg (content/shared/img/video-hover.svg)
content/shared/img/video-mute.svg (content/shared/img/video-mute.svg)
content/shared/img/video-mute-hover.svg (content/shared/img/video-mute-hover.svg)
content/shared/img/settings.svg (content/shared/img/settings.svg)
content/shared/img/settings-hover.svg (content/shared/img/settings-hover.svg)
content/shared/img/sharing.svg (content/shared/img/sharing.svg)
content/shared/img/sharing-active.svg (content/shared/img/sharing-active.svg)
content/shared/img/sharing-pending.svg (content/shared/img/sharing-pending.svg)
content/shared/img/sharing-hover.svg (content/shared/img/sharing-hover.svg)
content/shared/img/media-group.svg (content/shared/img/media-group.svg)
content/shared/img/media-group-left-hover.svg (content/shared/img/media-group-left-hover.svg)
content/shared/img/media-group-right-hover.svg (content/shared/img/media-group-right-hover.svg)
content/shared/img/audio-call-avatar.svg (content/shared/img/audio-call-avatar.svg)
content/shared/img/beta-ribbon.svg (content/shared/img/beta-ribbon.svg)
content/shared/img/check.svg (content/shared/img/check.svg)
content/shared/img/icons-10x10.svg (content/shared/img/icons-10x10.svg)
content/shared/img/icons-14x14.svg (content/shared/img/icons-14x14.svg)
content/shared/img/icons-16x16.svg (content/shared/img/icons-16x16.svg)
content/shared/img/movistar.png (content/shared/img/movistar.png)
content/shared/img/movistar@2x.png (content/shared/img/movistar@2x.png)
content/shared/img/vivo.png (content/shared/img/vivo.png)
content/shared/img/vivo@2x.png (content/shared/img/vivo@2x.png)
content/shared/img/02.png (content/shared/img/02.png)
content/shared/img/02@2x.png (content/shared/img/02@2x.png)
content/shared/img/telefonica-logo.svg (content/shared/img/telefonica-logo.svg)
content/shared/img/hello_logo.svg (content/shared/img/hello_logo.svg)
content/shared/img/hello-web-share.svg (content/shared/img/hello-web-share.svg)
content/shared/img/ellipsis-v.svg (content/shared/img/ellipsis-v.svg)
content/shared/img/empty_conversations.svg (content/shared/img/empty_conversations.svg)
content/shared/img/empty_search.svg (content/shared/img/empty_search.svg)
content/shared/img/animated-spinner.svg (content/shared/img/animated-spinner.svg)
content/shared/img/avatars.svg (content/shared/img/avatars.svg)
content/shared/img/firefox-avatar.svg (content/shared/img/firefox-avatar.svg)
content/shared/img/pause-12x12.svg (content/shared/img/pause-12x12.svg)
content/shared/img/play-12x12.svg (content/shared/img/play-12x12.svg)
content/shared/img/stop-12x12.svg (content/shared/img/stop-12x12.svg)
# Shared scripts
content/shared/js/actions.js (content/shared/js/actions.js)
content/shared/js/crypto.js (content/shared/js/crypto.js)
content/shared/js/store.js (content/shared/js/store.js)
content/shared/js/activeRoomStore.js (content/shared/js/activeRoomStore.js)
content/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
content/shared/js/linkifiedTextView.js (content/shared/js/linkifiedTextView.js)
content/shared/js/loopapi-client.js (content/shared/js/loopapi-client.js)
content/shared/js/models.js (content/shared/js/models.js)
content/shared/js/mixins.js (content/shared/js/mixins.js)
content/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js)
content/shared/js/views.js (content/shared/js/views.js)
content/shared/js/textChatStore.js (content/shared/js/textChatStore.js)
content/shared/js/textChatView.js (content/shared/js/textChatView.js)
content/shared/js/urlRegExps.js (content/shared/js/urlRegExps.js)
content/shared/js/utils.js (content/shared/js/utils.js)
content/shared/js/validate.js (content/shared/js/validate.js)
# Shared libs
#ifdef DEBUG
content/shared/vendor/react-0.13.3.js (content/shared/vendor/react-0.13.3.js)
#else
content/shared/vendor/react-0.13.3.js (content/shared/vendor/react-0.13.3-prod.js)
skin/ (chrome/skin/*)
content/modules/ (chrome/content/modules/*)
# We don't package the test/ directory for panels, so do these separately.
content/panels/ (chrome/content/panels/*.html)
content/panels/css/ (chrome/content/panels/css/*)
content/panels/js/ (chrome/content/panels/js/*)
content/panels/vendor/ (chrome/content/panels/vendor/*)
* content/preferences/prefs.js (chrome/content/preferences/prefs.js)
# We don't package the test/ directory for shared, so do these separately.
content/shared/css/ (chrome/content/shared/css/*)
content/shared/img/ (chrome/content/shared/img/*)
content/shared/js/ (chrome/content/shared/js/*)
content/shared/sounds/ (chrome/content/shared/sounds/*)
content/shared/vendor/ (chrome/content/shared/vendor/*)
#ifndef DEBUG
+ content/shared/vendor/react.js (chrome/content/shared/vendor/react-prod.js)
#endif
content/shared/vendor/lodash-3.9.3.js (content/shared/vendor/lodash-3.9.3.js)
content/shared/vendor/backbone-1.2.1.js (content/shared/vendor/backbone-1.2.1.js)
content/shared/vendor/classnames-2.2.0.js (content/shared/vendor/classnames-2.2.0.js)
# Shared sounds
content/shared/sounds/ringtone.ogg (content/shared/sounds/ringtone.ogg)
content/shared/sounds/connecting.ogg (content/shared/sounds/connecting.ogg)
content/shared/sounds/connected.ogg (content/shared/sounds/connected.ogg)
content/shared/sounds/terminated.ogg (content/shared/sounds/terminated.ogg)
content/shared/sounds/room-joined.ogg (content/shared/sounds/room-joined.ogg)
content/shared/sounds/room-joined-in.ogg (content/shared/sounds/room-joined-in.ogg)
content/shared/sounds/room-left.ogg (content/shared/sounds/room-left.ogg)
content/shared/sounds/failure.ogg (content/shared/sounds/failure.ogg)
content/shared/sounds/message.ogg (content/shared/sounds/message.ogg)
# Partner SDK assets
content/shared/vendor/sdk.js (content/shared/vendor/sdk.js)
content/sdk-content/js/dynamic_config.min.js (content/shared/vendor/sdk-content/js/dynamic_config.min.js)