mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-11 12:25:53 +00:00
Merge fx-team to m-c. a=merge
This commit is contained in:
commit
e4f7ef24d3
@ -13,11 +13,35 @@ The standalone client exists in standalone/ but shares items
|
||||
file in the standalone/ directory for how to run the server locally.
|
||||
|
||||
|
||||
Working with JSX
|
||||
================
|
||||
Hacking
|
||||
=======
|
||||
Please be sure to execute
|
||||
|
||||
You need to install the JSX compiler using npm in order to compile the .jsx
|
||||
files into regular .js ones:
|
||||
browser/components/loop/run-all-loop-tests.sh
|
||||
|
||||
from the top level before requesting review on a patch.
|
||||
|
||||
|
||||
Functional Tests
|
||||
================
|
||||
These are currently a work in progress, but it's already possible to run a test
|
||||
if you have a [loop-server](https://github.com/mozilla-services/loop-server)
|
||||
install that is properly configured. From the top-level gecko directory,
|
||||
execute:
|
||||
|
||||
export LOOP_SERVER=/Users/larry/src/loop-server
|
||||
./mach marionette-test browser/components/loop/test/functional/manifest.ini
|
||||
|
||||
Once the automation is complete, we'll include this in run-all-loop-tests.sh
|
||||
as well.
|
||||
|
||||
|
||||
Working with React JSX files
|
||||
============================
|
||||
|
||||
Our views use [React](http://facebook.github.io/react/) written in JSX files
|
||||
and transpiled to JS before we commit. You need to install the JSX compiler
|
||||
using npm in order to compile the .jsx files into regular .js ones:
|
||||
|
||||
npm install -g react-tools
|
||||
|
||||
|
@ -158,18 +158,34 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
incoming: function(loopVersion) {
|
||||
navigator.mozLoop.startAlerting();
|
||||
this._conversation.set({loopVersion: loopVersion});
|
||||
this._conversation.once("accept", function() {
|
||||
this._conversation.once("accept", () => {
|
||||
this.navigate("call/accept", {trigger: true});
|
||||
}.bind(this));
|
||||
this._conversation.once("decline", function() {
|
||||
});
|
||||
this._conversation.once("decline", () => {
|
||||
this.navigate("call/decline", {trigger: true});
|
||||
}.bind(this));
|
||||
this._conversation.once("declineAndBlock", function() {
|
||||
});
|
||||
this._conversation.once("declineAndBlock", () => {
|
||||
this.navigate("call/declineAndBlock", {trigger: true});
|
||||
}.bind(this));
|
||||
this.loadReactComponent(loop.conversation.IncomingCallView({
|
||||
model: this._conversation
|
||||
}));
|
||||
});
|
||||
this._conversation.once("call:incoming", this.startCall, this);
|
||||
this._client.requestCallsInfo(loopVersion, (err, sessionData) => {
|
||||
if (err) {
|
||||
console.error("Failed to get the sessionData", err);
|
||||
// XXX Not the ideal response, but bug 1047410 will be replacing
|
||||
//this by better "call failed" UI.
|
||||
this._notifier.errorL10n("cannot_start_call_session_not_ready");
|
||||
return;
|
||||
}
|
||||
// XXX For incoming calls we might have more than one call queued.
|
||||
// For now, we'll just assume the first call is the right information.
|
||||
// We'll probably really want to be getting this data from the
|
||||
// background worker on the desktop client.
|
||||
// Bug 1032700 should fix this.
|
||||
this._conversation.setSessionData(sessionData[0]);
|
||||
this.loadReactComponent(loop.conversation.IncomingCallView({
|
||||
model: this._conversation
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -177,10 +193,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
*/
|
||||
accept: function() {
|
||||
navigator.mozLoop.stopAlerting();
|
||||
this._conversation.initiate({
|
||||
client: new loop.Client(),
|
||||
outgoing: false
|
||||
});
|
||||
this._conversation.incoming();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -201,10 +214,10 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
declineAndBlock: function() {
|
||||
navigator.mozLoop.stopAlerting();
|
||||
var token = navigator.mozLoop.getLoopCharPref('loopToken');
|
||||
var client = new loop.Client();
|
||||
client.deleteCallUrl(token, function(error) {
|
||||
this._client.deleteCallUrl(token, function(error) {
|
||||
// XXX The conversation window will be closed when this cb is triggered
|
||||
// figure out if there is a better way to report the error to the user
|
||||
// (bug 1048909).
|
||||
console.log(error);
|
||||
});
|
||||
window.close();
|
||||
@ -254,8 +267,12 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
|
||||
document.title = mozL10n.get("incoming_call_title");
|
||||
|
||||
var client = new loop.Client();
|
||||
router = new ConversationRouter({
|
||||
conversation: new loop.shared.models.ConversationModel({}, {sdk: OT}),
|
||||
client: client,
|
||||
conversation: new loop.shared.models.ConversationModel(
|
||||
{}, // Model attributes
|
||||
{sdk: OT}), // Model dependencies
|
||||
notifier: new sharedViews.NotificationListView({el: "#messages"})
|
||||
});
|
||||
Backbone.history.start();
|
||||
|
@ -158,18 +158,34 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
incoming: function(loopVersion) {
|
||||
navigator.mozLoop.startAlerting();
|
||||
this._conversation.set({loopVersion: loopVersion});
|
||||
this._conversation.once("accept", function() {
|
||||
this._conversation.once("accept", () => {
|
||||
this.navigate("call/accept", {trigger: true});
|
||||
}.bind(this));
|
||||
this._conversation.once("decline", function() {
|
||||
});
|
||||
this._conversation.once("decline", () => {
|
||||
this.navigate("call/decline", {trigger: true});
|
||||
}.bind(this));
|
||||
this._conversation.once("declineAndBlock", function() {
|
||||
});
|
||||
this._conversation.once("declineAndBlock", () => {
|
||||
this.navigate("call/declineAndBlock", {trigger: true});
|
||||
}.bind(this));
|
||||
this.loadReactComponent(loop.conversation.IncomingCallView({
|
||||
model: this._conversation
|
||||
}));
|
||||
});
|
||||
this._conversation.once("call:incoming", this.startCall, this);
|
||||
this._client.requestCallsInfo(loopVersion, (err, sessionData) => {
|
||||
if (err) {
|
||||
console.error("Failed to get the sessionData", err);
|
||||
// XXX Not the ideal response, but bug 1047410 will be replacing
|
||||
//this by better "call failed" UI.
|
||||
this._notifier.errorL10n("cannot_start_call_session_not_ready");
|
||||
return;
|
||||
}
|
||||
// XXX For incoming calls we might have more than one call queued.
|
||||
// For now, we'll just assume the first call is the right information.
|
||||
// We'll probably really want to be getting this data from the
|
||||
// background worker on the desktop client.
|
||||
// Bug 1032700 should fix this.
|
||||
this._conversation.setSessionData(sessionData[0]);
|
||||
this.loadReactComponent(loop.conversation.IncomingCallView({
|
||||
model: this._conversation
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -177,10 +193,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
*/
|
||||
accept: function() {
|
||||
navigator.mozLoop.stopAlerting();
|
||||
this._conversation.initiate({
|
||||
client: new loop.Client(),
|
||||
outgoing: false
|
||||
});
|
||||
this._conversation.incoming();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -201,10 +214,10 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
declineAndBlock: function() {
|
||||
navigator.mozLoop.stopAlerting();
|
||||
var token = navigator.mozLoop.getLoopCharPref('loopToken');
|
||||
var client = new loop.Client();
|
||||
client.deleteCallUrl(token, function(error) {
|
||||
this._client.deleteCallUrl(token, function(error) {
|
||||
// XXX The conversation window will be closed when this cb is triggered
|
||||
// figure out if there is a better way to report the error to the user
|
||||
// (bug 1048909).
|
||||
console.log(error);
|
||||
});
|
||||
window.close();
|
||||
@ -254,8 +267,12 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
|
||||
document.title = mozL10n.get("incoming_call_title");
|
||||
|
||||
var client = new loop.Client();
|
||||
router = new ConversationRouter({
|
||||
conversation: new loop.shared.models.ConversationModel({}, {sdk: OT}),
|
||||
client: client,
|
||||
conversation: new loop.shared.models.ConversationModel(
|
||||
{}, // Model attributes
|
||||
{sdk: OT}), // Model dependencies
|
||||
notifier: new sharedViews.NotificationListView({el: "#messages"})
|
||||
});
|
||||
Backbone.history.start();
|
||||
|
@ -209,11 +209,15 @@ loop.panel = (function(_, mozL10n) {
|
||||
// readOnly attr will suppress a warning regarding this issue
|
||||
// from the react lib.
|
||||
var cx = React.addons.classSet;
|
||||
var inputCSSClass = {
|
||||
"pending": this.state.pending,
|
||||
"callUrl": !this.state.pending
|
||||
};
|
||||
return (
|
||||
PanelLayout({summary: __("share_link_header_text")},
|
||||
React.DOM.div({className: "invite"},
|
||||
React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true",
|
||||
className: cx({'pending': this.state.pending})})
|
||||
className: cx(inputCSSClass)})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -209,11 +209,15 @@ loop.panel = (function(_, mozL10n) {
|
||||
// readOnly attr will suppress a warning regarding this issue
|
||||
// from the react lib.
|
||||
var cx = React.addons.classSet;
|
||||
var inputCSSClass = {
|
||||
"pending": this.state.pending,
|
||||
"callUrl": !this.state.pending
|
||||
};
|
||||
return (
|
||||
<PanelLayout summary={__("share_link_header_text")}>
|
||||
<div className="invite">
|
||||
<input type="url" value={this.state.callUrl} readOnly="true"
|
||||
className={cx({'pending': this.state.pending})} />
|
||||
className={cx(inputCSSClass)} />
|
||||
</div>
|
||||
</PanelLayout>
|
||||
);
|
||||
|
@ -79,30 +79,28 @@ loop.shared.models = (function() {
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiates a conversation, requesting call session information to the Loop
|
||||
* server and updates appropriately the current model attributes with the
|
||||
* data.
|
||||
*
|
||||
* Available options:
|
||||
*
|
||||
* - {Boolean} outgoing Set to true if this model represents the
|
||||
* outgoing call.
|
||||
* - {Boolean} callType Only valid for outgoing calls. The type of media in
|
||||
* the call, e.g. "audio" or "audio-video"
|
||||
* - {loop.shared.Client} client A client object to request call information
|
||||
* from. Expects requestCallInfo for outgoing
|
||||
* calls, requestCallsInfo for incoming calls.
|
||||
*
|
||||
* Triggered events:
|
||||
*
|
||||
* - `session:ready` when the session information have been successfully
|
||||
* retrieved from the server;
|
||||
* - `session:error` when the request failed.
|
||||
*
|
||||
* @param {Object} options Options object
|
||||
* Starts an incoming conversation.
|
||||
*/
|
||||
initiate: function(options) {
|
||||
options = options || {};
|
||||
incoming: function() {
|
||||
this.trigger("call:incoming");
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to indicate that an outgoing call should start any necessary
|
||||
* set-up.
|
||||
*/
|
||||
setupOutgoingCall: function() {
|
||||
this.trigger("call:outgoing:setup");
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts an outgoing conversation.
|
||||
*
|
||||
* @param {Object} sessionData The session data received from the
|
||||
* server for the outgoing call.
|
||||
*/
|
||||
outgoing: function(sessionData) {
|
||||
this._clearPendingCallTimer();
|
||||
|
||||
// Outgoing call has never reached destination, closing - see bug 1020448
|
||||
function handleOutgoingCallTimeout() {
|
||||
@ -112,39 +110,12 @@ loop.shared.models = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleResult(err, sessionData) {
|
||||
/*jshint validthis:true */
|
||||
this._clearPendingCallTimer();
|
||||
// Setup pending call timeout.
|
||||
this._pendingCallTimer = setTimeout(
|
||||
handleOutgoingCallTimeout.bind(this), this.pendingCallTimeout);
|
||||
|
||||
if (err) {
|
||||
this._handleServerError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.outgoing) {
|
||||
// Setup pending call timeout.
|
||||
this._pendingCallTimer = setTimeout(
|
||||
handleOutgoingCallTimeout.bind(this), this.pendingCallTimeout);
|
||||
} else {
|
||||
// XXX For incoming calls we might have more than one call queued.
|
||||
// For now, we'll just assume the first call is the right information.
|
||||
// We'll probably really want to be getting this data from the
|
||||
// background worker on the desktop client.
|
||||
// Bug 990714 should fix this.
|
||||
sessionData = sessionData[0];
|
||||
}
|
||||
|
||||
this.setReady(sessionData);
|
||||
}
|
||||
|
||||
if (options.outgoing) {
|
||||
options.client.requestCallInfo(this.get("loopToken"), options.callType,
|
||||
handleResult.bind(this));
|
||||
}
|
||||
else {
|
||||
options.client.requestCallsInfo(this.get("loopVersion"),
|
||||
handleResult.bind(this));
|
||||
}
|
||||
this.setSessionData(sessionData);
|
||||
this.trigger("call:outgoing");
|
||||
},
|
||||
|
||||
/**
|
||||
@ -157,18 +128,17 @@ loop.shared.models = (function() {
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets session information and triggers the `session:ready` event.
|
||||
* Sets session information.
|
||||
*
|
||||
* @param {Object} sessionData Conversation session information.
|
||||
*/
|
||||
setReady: function(sessionData) {
|
||||
setSessionData: function(sessionData) {
|
||||
// Explicit property assignment to prevent later "surprises"
|
||||
this.set({
|
||||
sessionId: sessionData.sessionId,
|
||||
sessionToken: sessionData.sessionToken,
|
||||
apiKey: sessionData.apiKey
|
||||
}).trigger("session:ready", this);
|
||||
return this;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -121,9 +121,12 @@ loop.shared.router = (function(l10n) {
|
||||
if (!options.conversation) {
|
||||
throw new Error("missing required conversation");
|
||||
}
|
||||
if (!options.client) {
|
||||
throw new Error("missing required client");
|
||||
}
|
||||
this._conversation = options.conversation;
|
||||
this._client = options.client;
|
||||
|
||||
this.listenTo(this._conversation, "session:ready", this._onSessionReady);
|
||||
this.listenTo(this._conversation, "session:ended", this._onSessionEnded);
|
||||
this.listenTo(this._conversation, "session:peer-hungup",
|
||||
this._onPeerHungup);
|
||||
@ -145,23 +148,11 @@ loop.shared.router = (function(l10n) {
|
||||
this.endCall();
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts the call. This method should be overriden.
|
||||
*/
|
||||
startCall: function() {},
|
||||
|
||||
/**
|
||||
* Ends the call. This method should be overriden.
|
||||
*/
|
||||
endCall: function() {},
|
||||
|
||||
/**
|
||||
* Session is ready.
|
||||
*/
|
||||
_onSessionReady: function() {
|
||||
this.startCall();
|
||||
},
|
||||
|
||||
/**
|
||||
* Session has ended. Notifies the user and ends the call.
|
||||
*/
|
||||
|
@ -15,8 +15,8 @@ test:
|
||||
lint:
|
||||
@$(NODE_LOCAL_BIN)/jshint *.js content test
|
||||
|
||||
runserver: config
|
||||
@node server.js
|
||||
runserver: remove_old_config
|
||||
node server.js
|
||||
|
||||
frontend:
|
||||
@echo "Not implemented yet."
|
||||
@ -36,6 +36,19 @@ version:
|
||||
@echo $(SOURCE_STAMP) > content/VERSION.txt
|
||||
@echo $(SOURCE_DATE) >> content/VERSION.txt
|
||||
|
||||
|
||||
# The local node server used for client dev (server.js) used to use a static
|
||||
# content/config.js. Now that information is server up dynamically. This
|
||||
# target is depended on by runserver, and removes any copies of that to avoid
|
||||
# confusion.
|
||||
remove_old_config:
|
||||
@rm -f content/config.js
|
||||
|
||||
|
||||
# The services development deployment, however, still wants a static config
|
||||
# file, and needs an easy way to generate one. This target is for folks
|
||||
# working with that deployment.
|
||||
.PHONY: config
|
||||
config:
|
||||
@echo "var loop = loop || {};" > content/config.js
|
||||
@echo "loop.config = loop.config || {};" >> content/config.js
|
||||
|
@ -14,7 +14,8 @@ Installation
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
You will need to generate a configuration file, you can do so with:
|
||||
If you need a static config.js file for deployment (most people wont; only
|
||||
folks deploying the development server will!), you can generate one like this:
|
||||
|
||||
$ make config
|
||||
|
||||
|
@ -131,7 +131,7 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
* Conversation launcher view. A ConversationModel is associated and attached
|
||||
* as a `model` property.
|
||||
*/
|
||||
var ConversationFormView = React.createClass({displayName: 'ConversationFormView',
|
||||
var StartConversationView = React.createClass({displayName: 'StartConversationView',
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@ -174,19 +174,9 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
/**
|
||||
* Initiates the call.
|
||||
*/
|
||||
_initiate: function() {
|
||||
this.props.model.initiate({
|
||||
client: new loop.StandaloneClient({
|
||||
baseServerUrl: baseServerUrl
|
||||
}),
|
||||
outgoing: true,
|
||||
// For now, we assume both audio and video as there is no
|
||||
// other option to select.
|
||||
callType: "audio-video",
|
||||
loopServer: loop.config.serverUrl
|
||||
});
|
||||
|
||||
_initiateOutgoingCall: function() {
|
||||
this.setState({disableCallButton: true});
|
||||
this.props.model.setupOutgoingCall();
|
||||
},
|
||||
|
||||
_setConversationTimestamp: function(err, callUrlInfo) {
|
||||
@ -231,7 +221,7 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
|
||||
React.DOM.div({className: "button-group"},
|
||||
React.DOM.div({className: "flex-padding-1"}),
|
||||
React.DOM.button({ref: "submitButton", onClick: this._initiate,
|
||||
React.DOM.button({ref: "submitButton", onClick: this._initiateOutgoingCall,
|
||||
className: callButtonClasses,
|
||||
disabled: this.state.disableCallButton},
|
||||
__("initiate_call_button"),
|
||||
@ -267,15 +257,13 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
initialize: function(options) {
|
||||
this.helper = options.helper;
|
||||
if (!this.helper) {
|
||||
throw new Error("WebappRouter requires an helper object");
|
||||
throw new Error("WebappRouter requires a helper object");
|
||||
}
|
||||
|
||||
// Load default view
|
||||
this.loadView(new HomeView());
|
||||
|
||||
this.listenTo(this._conversation, "timeout", this._onTimeout);
|
||||
this.listenTo(this._conversation, "session:expired",
|
||||
this._onSessionExpired);
|
||||
},
|
||||
|
||||
_onSessionExpired: function() {
|
||||
@ -283,14 +271,51 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {loop.shared.router.BaseConversationRouter.startCall}
|
||||
* Starts the set up of a call, obtaining the required information from the
|
||||
* server.
|
||||
*/
|
||||
startCall: function() {
|
||||
if (!this._conversation.get("loopToken")) {
|
||||
setupOutgoingCall: function() {
|
||||
var loopToken = this._conversation.get("loopToken");
|
||||
if (!loopToken) {
|
||||
this._notifier.errorL10n("missing_conversation_info");
|
||||
this.navigate("home", {trigger: true});
|
||||
} else {
|
||||
this.navigate("call/ongoing/" + this._conversation.get("loopToken"), {
|
||||
this._conversation.once("call:outgoing", this.startCall, this);
|
||||
|
||||
// XXX For now, we assume both audio and video as there is no
|
||||
// other option to select (bug 1048333)
|
||||
this._client.requestCallInfo(this._conversation.get("loopToken"), "audio-video",
|
||||
(err, sessionData) => {
|
||||
if (err) {
|
||||
switch (err.errno) {
|
||||
// loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
|
||||
// missing OR expired; we treat this information as if the url is always
|
||||
// expired.
|
||||
case 105:
|
||||
this._onSessionExpired();
|
||||
break;
|
||||
default:
|
||||
this._notifier.errorL10n("missing_conversation_info");
|
||||
this.navigate("home", {trigger: true});
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._conversation.outgoing(sessionData);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Actually starts the call.
|
||||
*/
|
||||
startCall: function() {
|
||||
var loopToken = this._conversation.get("loopToken");
|
||||
if (!loopToken) {
|
||||
this._notifier.errorL10n("missing_conversation_info");
|
||||
this.navigate("home", {trigger: true});
|
||||
} else {
|
||||
this.navigate("call/ongoing/" + loopToken, {
|
||||
trigger: true
|
||||
});
|
||||
}
|
||||
@ -343,13 +368,14 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
this._conversation.endSession();
|
||||
}
|
||||
this._conversation.set("loopToken", loopToken);
|
||||
this.loadReactComponent(ConversationFormView({
|
||||
|
||||
var startView = StartConversationView({
|
||||
model: this._conversation,
|
||||
notifier: this._notifier,
|
||||
client: new loop.StandaloneClient({
|
||||
baseServerUrl: loop.config.serverUrl
|
||||
})
|
||||
}));
|
||||
client: this._client
|
||||
});
|
||||
this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
|
||||
this.loadReactComponent(startView);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -390,9 +416,13 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
*/
|
||||
function init() {
|
||||
var helper = new WebappHelper();
|
||||
var client = new loop.StandaloneClient({
|
||||
baseServerUrl: baseServerUrl
|
||||
}),
|
||||
router = new WebappRouter({
|
||||
helper: helper,
|
||||
notifier: new sharedViews.NotificationListView({el: "#messages"}),
|
||||
client: client,
|
||||
conversation: new sharedModels.ConversationModel({}, {
|
||||
sdk: OT,
|
||||
pendingCallTimeout: loop.config.pendingCallTimeout
|
||||
@ -412,7 +442,7 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
return {
|
||||
baseServerUrl: baseServerUrl,
|
||||
CallUrlExpiredView: CallUrlExpiredView,
|
||||
ConversationFormView: ConversationFormView,
|
||||
StartConversationView: StartConversationView,
|
||||
HomeView: HomeView,
|
||||
init: init,
|
||||
PromoteFirefoxView: PromoteFirefoxView,
|
||||
|
@ -131,7 +131,7 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
* Conversation launcher view. A ConversationModel is associated and attached
|
||||
* as a `model` property.
|
||||
*/
|
||||
var ConversationFormView = React.createClass({
|
||||
var StartConversationView = React.createClass({
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@ -174,19 +174,9 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
/**
|
||||
* Initiates the call.
|
||||
*/
|
||||
_initiate: function() {
|
||||
this.props.model.initiate({
|
||||
client: new loop.StandaloneClient({
|
||||
baseServerUrl: baseServerUrl
|
||||
}),
|
||||
outgoing: true,
|
||||
// For now, we assume both audio and video as there is no
|
||||
// other option to select.
|
||||
callType: "audio-video",
|
||||
loopServer: loop.config.serverUrl
|
||||
});
|
||||
|
||||
_initiateOutgoingCall: function() {
|
||||
this.setState({disableCallButton: true});
|
||||
this.props.model.setupOutgoingCall();
|
||||
},
|
||||
|
||||
_setConversationTimestamp: function(err, callUrlInfo) {
|
||||
@ -231,7 +221,7 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
|
||||
<div className="button-group">
|
||||
<div className="flex-padding-1"></div>
|
||||
<button ref="submitButton" onClick={this._initiate}
|
||||
<button ref="submitButton" onClick={this._initiateOutgoingCall}
|
||||
className={callButtonClasses}
|
||||
disabled={this.state.disableCallButton}>
|
||||
{__("initiate_call_button")}
|
||||
@ -267,15 +257,13 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
initialize: function(options) {
|
||||
this.helper = options.helper;
|
||||
if (!this.helper) {
|
||||
throw new Error("WebappRouter requires an helper object");
|
||||
throw new Error("WebappRouter requires a helper object");
|
||||
}
|
||||
|
||||
// Load default view
|
||||
this.loadView(new HomeView());
|
||||
|
||||
this.listenTo(this._conversation, "timeout", this._onTimeout);
|
||||
this.listenTo(this._conversation, "session:expired",
|
||||
this._onSessionExpired);
|
||||
},
|
||||
|
||||
_onSessionExpired: function() {
|
||||
@ -283,14 +271,51 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {loop.shared.router.BaseConversationRouter.startCall}
|
||||
* Starts the set up of a call, obtaining the required information from the
|
||||
* server.
|
||||
*/
|
||||
startCall: function() {
|
||||
if (!this._conversation.get("loopToken")) {
|
||||
setupOutgoingCall: function() {
|
||||
var loopToken = this._conversation.get("loopToken");
|
||||
if (!loopToken) {
|
||||
this._notifier.errorL10n("missing_conversation_info");
|
||||
this.navigate("home", {trigger: true});
|
||||
} else {
|
||||
this.navigate("call/ongoing/" + this._conversation.get("loopToken"), {
|
||||
this._conversation.once("call:outgoing", this.startCall, this);
|
||||
|
||||
// XXX For now, we assume both audio and video as there is no
|
||||
// other option to select (bug 1048333)
|
||||
this._client.requestCallInfo(this._conversation.get("loopToken"), "audio-video",
|
||||
(err, sessionData) => {
|
||||
if (err) {
|
||||
switch (err.errno) {
|
||||
// loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
|
||||
// missing OR expired; we treat this information as if the url is always
|
||||
// expired.
|
||||
case 105:
|
||||
this._onSessionExpired();
|
||||
break;
|
||||
default:
|
||||
this._notifier.errorL10n("missing_conversation_info");
|
||||
this.navigate("home", {trigger: true});
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._conversation.outgoing(sessionData);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Actually starts the call.
|
||||
*/
|
||||
startCall: function() {
|
||||
var loopToken = this._conversation.get("loopToken");
|
||||
if (!loopToken) {
|
||||
this._notifier.errorL10n("missing_conversation_info");
|
||||
this.navigate("home", {trigger: true});
|
||||
} else {
|
||||
this.navigate("call/ongoing/" + loopToken, {
|
||||
trigger: true
|
||||
});
|
||||
}
|
||||
@ -343,13 +368,14 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
this._conversation.endSession();
|
||||
}
|
||||
this._conversation.set("loopToken", loopToken);
|
||||
this.loadReactComponent(ConversationFormView({
|
||||
|
||||
var startView = StartConversationView({
|
||||
model: this._conversation,
|
||||
notifier: this._notifier,
|
||||
client: new loop.StandaloneClient({
|
||||
baseServerUrl: loop.config.serverUrl
|
||||
})
|
||||
}));
|
||||
client: this._client
|
||||
});
|
||||
this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
|
||||
this.loadReactComponent(startView);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -390,9 +416,13 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
*/
|
||||
function init() {
|
||||
var helper = new WebappHelper();
|
||||
var client = new loop.StandaloneClient({
|
||||
baseServerUrl: baseServerUrl
|
||||
}),
|
||||
router = new WebappRouter({
|
||||
helper: helper,
|
||||
notifier: new sharedViews.NotificationListView({el: "#messages"}),
|
||||
client: client,
|
||||
conversation: new sharedModels.ConversationModel({}, {
|
||||
sdk: OT,
|
||||
pendingCallTimeout: loop.config.pendingCallTimeout
|
||||
@ -412,7 +442,7 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
return {
|
||||
baseServerUrl: baseServerUrl,
|
||||
CallUrlExpiredView: CallUrlExpiredView,
|
||||
ConversationFormView: ConversationFormView,
|
||||
StartConversationView: StartConversationView,
|
||||
HomeView: HomeView,
|
||||
init: init,
|
||||
PromoteFirefoxView: PromoteFirefoxView,
|
||||
|
@ -5,13 +5,51 @@
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
|
||||
// This lets /test/ be mapped to the right place for running tests
|
||||
app.use(express.static(__dirname + '/../'));
|
||||
// This lets /content/ be mappy right for the static contents.
|
||||
app.use(express.static(__dirname + '/'));
|
||||
var port = process.env.PORT || 3000;
|
||||
var loopServerPort = process.env.LOOP_SERVER_PORT || 5000;
|
||||
|
||||
app.listen(3000);
|
||||
console.log("Serving repository root over HTTP at http://localhost:3000/");
|
||||
console.log("Static contents are available at http://localhost:3000/content/");
|
||||
console.log("Tests are viewable at http://localhost:3000/test/");
|
||||
app.get('/content/config.js', function (req, res) {
|
||||
"use strict";
|
||||
|
||||
res.set('Content-Type', 'text/javascript');
|
||||
res.send(
|
||||
"var loop = loop || {};" +
|
||||
"loop.config = loop.config || {};" +
|
||||
"loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';" +
|
||||
"loop.config.pendingCallTimeout = 20000;"
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
// This lets /test/ be mapped to the right place for running tests
|
||||
app.use('/', express.static(__dirname + '/../'));
|
||||
// This lets /content/ be mapped right for the static contents.
|
||||
app.use('/', express.static(__dirname + '/'));
|
||||
|
||||
var server = app.listen(port);
|
||||
|
||||
var baseUrl = "http://localhost:" + port + "/";
|
||||
|
||||
console.log("Serving repository root over HTTP at " + baseUrl);
|
||||
console.log("Static contents are available at " + baseUrl + "content/");
|
||||
console.log("Tests are viewable at " + baseUrl + "test/");
|
||||
console.log("Use this for development only.");
|
||||
|
||||
// Handle SIGTERM signal.
|
||||
function shutdown(cb) {
|
||||
"use strict";
|
||||
|
||||
try {
|
||||
server.close(function () {
|
||||
process.exit(0);
|
||||
if (cb !== undefined) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (ex) {
|
||||
console.log(ex + " while calling server.close)");
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGTERM', shutdown);
|
||||
|
@ -103,14 +103,16 @@ describe("loop.conversation", function() {
|
||||
});
|
||||
|
||||
describe("ConversationRouter", function() {
|
||||
var conversation;
|
||||
var conversation, client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.Client();
|
||||
conversation = new loop.shared.models.ConversationModel({}, {
|
||||
sdk: {},
|
||||
pendingCallTimeout: 1000
|
||||
pendingCallTimeout: 1000,
|
||||
});
|
||||
sandbox.stub(conversation, "initiate");
|
||||
sandbox.stub(client, "requestCallsInfo");
|
||||
sandbox.stub(conversation, "setSessionData");
|
||||
});
|
||||
|
||||
describe("Routes", function() {
|
||||
@ -118,10 +120,12 @@ describe("loop.conversation", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
router = new ConversationRouter({
|
||||
client: client,
|
||||
conversation: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
sandbox.stub(router, "loadView");
|
||||
sandbox.stub(conversation, "incoming");
|
||||
});
|
||||
|
||||
describe("#incoming", function() {
|
||||
@ -144,13 +148,58 @@ describe("loop.conversation", function() {
|
||||
stubComponent(loop.conversation, "IncomingCallView");
|
||||
});
|
||||
|
||||
it("should start alerting", function() {
|
||||
sandbox.stub(navigator.mozLoop, "startAlerting");
|
||||
router.incoming("fakeVersion");
|
||||
|
||||
sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
|
||||
});
|
||||
|
||||
it("should set the loopVersion on the conversation model", function() {
|
||||
router.incoming("fakeVersion");
|
||||
|
||||
expect(conversation.get("loopVersion")).to.equal("fakeVersion");
|
||||
});
|
||||
|
||||
it("should display the incoming call view", function() {
|
||||
it("should call requestCallsInfo on the client",
|
||||
function() {
|
||||
router.incoming(42);
|
||||
|
||||
sinon.assert.calledOnce(client.requestCallsInfo);
|
||||
sinon.assert.calledWith(client.requestCallsInfo, 42);
|
||||
});
|
||||
|
||||
it("should display an error if requestCallsInfo returns an error",
|
||||
function(){
|
||||
client.requestCallsInfo.callsArgWith(1, "failed");
|
||||
|
||||
router.incoming(42);
|
||||
|
||||
sinon.assert.calledOnce(notifier.errorL10n);
|
||||
});
|
||||
|
||||
describe("requestCallsInfo successful", function() {
|
||||
var fakeSessionData;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeSessionData = {
|
||||
sessionId: "sessionId",
|
||||
sessionToken: "sessionToken",
|
||||
apiKey: "apiKey"
|
||||
};
|
||||
|
||||
client.requestCallsInfo.callsArgWith(1, null, [fakeSessionData]);
|
||||
});
|
||||
|
||||
it("should store the session data", function() {
|
||||
router.incoming(42);
|
||||
|
||||
sinon.assert.calledOnce(conversation.setSessionData);
|
||||
sinon.assert.calledWithExactly(conversation.setSessionData,
|
||||
fakeSessionData);
|
||||
});
|
||||
|
||||
it("should display the incoming call view", function() {
|
||||
router.incoming("fakeVersion");
|
||||
|
||||
sinon.assert.calledOnce(loop.conversation.IncomingCallView);
|
||||
@ -162,13 +211,7 @@ describe("loop.conversation", function() {
|
||||
return TestUtils.isDescriptorOfType(value,
|
||||
loop.conversation.IncomingCallView);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should start alerting", function() {
|
||||
sandbox.stub(navigator.mozLoop, "startAlerting");
|
||||
router.incoming("fakeVersion");
|
||||
|
||||
sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -176,14 +219,7 @@ describe("loop.conversation", function() {
|
||||
it("should initiate the conversation", function() {
|
||||
router.accept();
|
||||
|
||||
sinon.assert.calledOnce(conversation.initiate);
|
||||
sinon.assert.calledWithMatch(conversation.initiate, {
|
||||
client: {
|
||||
mozLoop: navigator.mozLoop,
|
||||
settings: {}
|
||||
},
|
||||
outgoing: false
|
||||
});
|
||||
sinon.assert.calledOnce(conversation.incoming);
|
||||
});
|
||||
|
||||
it("should stop alerting", function() {
|
||||
@ -337,14 +373,17 @@ describe("loop.conversation", function() {
|
||||
"navigate");
|
||||
conversation.set("loopToken", "fakeToken");
|
||||
router = new loop.conversation.ConversationRouter({
|
||||
client: client,
|
||||
conversation: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
});
|
||||
|
||||
it("should navigate to call/ongoing once the call session is ready",
|
||||
it("should navigate to call/ongoing once the call is ready",
|
||||
function() {
|
||||
conversation.setReady(fakeSessionData);
|
||||
router.incoming(42);
|
||||
|
||||
conversation.incoming();
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ongoing");
|
||||
|
27
browser/components/loop/test/functional/hanging_threads.py
Normal file
27
browser/components/loop/test/functional/hanging_threads.py
Normal file
@ -0,0 +1,27 @@
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
# XXX We want to convince ourselves that the race condition this file works
|
||||
# around, tracked by bug 1046873, is gone, and get rid of this file entirely
|
||||
# before we hook this up to Tbpl, so that we don't introduce an intermittent
|
||||
# failure.
|
||||
#
|
||||
# reduced down from <https://gist.github.com/niccokunzmann/6038331>; importing
|
||||
# this class just so happens to allow mach to exit rather than hanging after
|
||||
# our tests are run.
|
||||
|
||||
|
||||
def monitor():
|
||||
while 1:
|
||||
time.sleep(1.)
|
||||
_ = sys._current_frames()
|
||||
|
||||
|
||||
def start_monitoring():
|
||||
thread = threading.Thread(target=monitor)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
start_monitoring()
|
6
browser/components/loop/test/functional/manifest.ini
Normal file
6
browser/components/loop/test/functional/manifest.ini
Normal file
@ -0,0 +1,6 @@
|
||||
[DEFAULT]
|
||||
b2g = false
|
||||
browser = true
|
||||
qemu = false
|
||||
|
||||
[test_get_url.py]
|
145
browser/components/loop/test/functional/test_get_url.py
Normal file
145
browser/components/loop/test/functional/test_get_url.py
Normal file
@ -0,0 +1,145 @@
|
||||
from marionette_test import MarionetteTestCase
|
||||
from marionette.errors import NoSuchElementException
|
||||
from marionette.errors import StaleElementException
|
||||
from by import By
|
||||
# noinspection PyUnresolvedReferences
|
||||
from marionette.wait import Wait
|
||||
import os
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from mozprocess import processhandler
|
||||
import urlparse
|
||||
|
||||
# XXX We want to convince ourselves that the race condition that importing
|
||||
# hanging_threads.py works around, tracked by bug 1046873, is gone, and get
|
||||
# rid of this inclusion entirely before we hook our functional tests up to
|
||||
# Tbpl, so that we don't introduce an intermittent failure.
|
||||
import sys
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
import hanging_threads
|
||||
|
||||
CONTENT_SERVER_PORT = 3001
|
||||
LOOP_SERVER_PORT = 5001
|
||||
|
||||
CONTENT_SERVER_COMMAND = ["make", "runserver"]
|
||||
CONTENT_SERVER_ENV = os.environ.copy()
|
||||
# Set PORT so that it does not interfere with any other
|
||||
# development server that might be running
|
||||
CONTENT_SERVER_ENV.update({"PORT": str(CONTENT_SERVER_PORT),
|
||||
"LOOP_SERVER_PORT": str(LOOP_SERVER_PORT)})
|
||||
|
||||
WEB_APP_URL = "http://localhost:" + str(CONTENT_SERVER_PORT) + \
|
||||
"/content/#call/{token}"
|
||||
|
||||
LOOP_SERVER_COMMAND = ["make", "runserver"]
|
||||
LOOP_SERVER_ENV = os.environ.copy()
|
||||
# Set PORT so that it does not interfere with any other
|
||||
# development server that might be running
|
||||
LOOP_SERVER_ENV.update({"NODE_ENV": "dev", "PORT": str(LOOP_SERVER_PORT),
|
||||
"WEB_APP_URL": WEB_APP_URL})
|
||||
|
||||
|
||||
class LoopTestServers:
|
||||
def __init__(self):
|
||||
self.loop_server = self.start_loop_server()
|
||||
self.content_server = self.start_content_server()
|
||||
|
||||
@staticmethod
|
||||
def start_loop_server():
|
||||
loop_server_location = os.environ.get('LOOP_SERVER')
|
||||
if loop_server_location is None:
|
||||
raise Exception('LOOP_SERVER variable not set')
|
||||
|
||||
os.chdir(loop_server_location)
|
||||
|
||||
p = processhandler.ProcessHandler(LOOP_SERVER_COMMAND,
|
||||
env=LOOP_SERVER_ENV)
|
||||
p.run()
|
||||
return p
|
||||
|
||||
@staticmethod
|
||||
def start_content_server():
|
||||
content_server_location = os.path.join(os.path.dirname(__file__),
|
||||
"../../standalone")
|
||||
os.chdir(content_server_location)
|
||||
|
||||
p = processhandler.ProcessHandler(CONTENT_SERVER_COMMAND,
|
||||
env=CONTENT_SERVER_ENV)
|
||||
p.run()
|
||||
return p
|
||||
|
||||
def shutdown(self):
|
||||
self.content_server.kill()
|
||||
self.loop_server.kill()
|
||||
|
||||
|
||||
class TestGetUrl(MarionetteTestCase):
|
||||
# XXX Move this to setup class so it doesn't restart the server
|
||||
# after every test. This can happen when we write our second test,
|
||||
# expected to be in bug 976116.
|
||||
def setUp(self):
|
||||
# start server
|
||||
self.loop_test_servers = LoopTestServers()
|
||||
|
||||
MarionetteTestCase.setUp(self)
|
||||
|
||||
# Unfortunately, enforcing preferences currently comes with the side
|
||||
# effect of launching and restarting the browser before running the
|
||||
# real functional tests. Bug 1048554 has been filed to track this.
|
||||
preferences = {"loop.server": "http://localhost:" + str(LOOP_SERVER_PORT)}
|
||||
self.marionette.enforce_gecko_prefs(preferences)
|
||||
|
||||
# this is browser chrome, kids, not the content window just yet
|
||||
self.marionette.set_context("chrome")
|
||||
|
||||
def switch_to_panel(self):
|
||||
button = self.marionette.find_element(By.ID, "loop-call-button")
|
||||
|
||||
# click the element
|
||||
button.click()
|
||||
|
||||
# switch to the frame
|
||||
frame = self.marionette.find_element(By.ID, "loop")
|
||||
self.marionette.switch_to_frame(frame)
|
||||
|
||||
# taken from https://github.com/mozilla-b2g/gaia/blob/master/tests/python/gaia-ui-tests/gaiatest/gaia_test.py#L858
|
||||
# XXX factor out into utility object for use by other tests, this can
|
||||
# when we write our second test, in bug 976116
|
||||
def wait_for_element_displayed(self, by, locator, timeout=None):
|
||||
Wait(self.marionette, timeout,
|
||||
ignored_exceptions=[NoSuchElementException, StaleElementException])\
|
||||
.until(lambda m: m.find_element(by, locator).is_displayed())
|
||||
return self.marionette.find_element(by, locator)
|
||||
|
||||
def test_get_url(self):
|
||||
self.switch_to_panel()
|
||||
|
||||
# get and check for a call url
|
||||
url_input_element = self.wait_for_element_displayed(By.TAG_NAME,
|
||||
"input")
|
||||
|
||||
# wait for pending state to finish
|
||||
self.assertEqual(url_input_element.get_attribute("class"), "pending",
|
||||
"expect the input to be pending")
|
||||
|
||||
# get and check the input (the "callUrl" class is only added after
|
||||
# the pending class is removed and the URL has arrived).
|
||||
#
|
||||
# XXX should investigate getting rid of the fragile and otherwise
|
||||
# unnecessary callUrl class and replacing this with a By.CSS_SELECTOR
|
||||
# and some possible combination of :not and/or an attribute selector
|
||||
# once bug 1048551 is fixed.
|
||||
url_input_element = self.wait_for_element_displayed(By.CLASS_NAME,
|
||||
"callUrl")
|
||||
call_url = url_input_element.get_attribute("value")
|
||||
self.assertNotEqual(call_url, u'',
|
||||
"input is populated with call URL after pending"
|
||||
" is finished")
|
||||
|
||||
self.assertIn(urlparse.urlparse(call_url).scheme, ['http', 'https'],
|
||||
"call URL returned by server " + call_url +
|
||||
" has invalid scheme")
|
||||
|
||||
def tearDown(self):
|
||||
self.loop_test_servers.shutdown()
|
||||
MarionetteTestCase.tearDown(self)
|
@ -78,114 +78,48 @@ describe("loop.shared.models", function() {
|
||||
requestCallsInfoStub = fakeClient.requestCallsInfo;
|
||||
});
|
||||
|
||||
describe("#initiate", function() {
|
||||
describe("#incoming", function() {
|
||||
it("should trigger a `call:incoming` event", function(done) {
|
||||
conversation.once("call:incoming", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
conversation.incoming();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setupOutgoingCall", function() {
|
||||
it("should trigger a `call:outgoing:setup` event", function(done) {
|
||||
conversation.once("call:outgoing:setup", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
conversation.setupOutgoingCall();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#outgoing", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(conversation, "endSession");
|
||||
sandbox.stub(conversation, "setSessionData");
|
||||
});
|
||||
|
||||
it("call requestCallInfo on the client for outgoing calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
client: fakeClient,
|
||||
outgoing: true,
|
||||
callType: "audio"
|
||||
});
|
||||
it("should save the sessionData", function() {
|
||||
conversation.outgoing(fakeSessionData);
|
||||
|
||||
sinon.assert.calledOnce(requestCallInfoStub);
|
||||
sinon.assert.calledWith(requestCallInfoStub, "fakeToken", "audio");
|
||||
sinon.assert.calledOnce(conversation.setSessionData);
|
||||
});
|
||||
|
||||
it("should trigger a `call:outgoing` event", function(done) {
|
||||
conversation.once("call:outgoing", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
it("should not call requestCallsInfo on the client for outgoing calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
client: fakeClient,
|
||||
outgoing: true,
|
||||
callType: "audio"
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(requestCallsInfoStub);
|
||||
});
|
||||
|
||||
it("call requestCallsInfo on the client for incoming calls",
|
||||
function() {
|
||||
conversation.set("loopVersion", 42);
|
||||
conversation.initiate({
|
||||
client: fakeClient,
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(requestCallsInfoStub);
|
||||
sinon.assert.calledWith(requestCallsInfoStub, 42);
|
||||
});
|
||||
|
||||
it("should not call requestCallInfo on the client for incoming calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
client: fakeClient,
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(requestCallInfoStub);
|
||||
});
|
||||
|
||||
it("should update conversation session information from server data",
|
||||
function() {
|
||||
sandbox.stub(conversation, "setReady");
|
||||
requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
|
||||
|
||||
conversation.initiate({
|
||||
client: fakeClient,
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(conversation.setReady);
|
||||
sinon.assert.calledWith(conversation.setReady, fakeSessionData);
|
||||
});
|
||||
|
||||
it("should trigger a `session:error` event errno is undefined",
|
||||
function(done) {
|
||||
var errMsg = "HTTP 500 Server Error; fake";
|
||||
var err = new Error(errMsg);
|
||||
requestCallInfoStub.callsArgWith(2, err);
|
||||
|
||||
conversation.on("session:error", function(err) {
|
||||
expect(err.message).eql(errMsg);
|
||||
done();
|
||||
}).initiate({ client: fakeClient, outgoing: true });
|
||||
});
|
||||
|
||||
it("should trigger a `session:error` event when errno is not 105",
|
||||
function(done) {
|
||||
var errMsg = "HTTP 400 Bad Request; fake";
|
||||
var err = new Error(errMsg);
|
||||
err.errno = 101;
|
||||
requestCallInfoStub.callsArgWith(2, err);
|
||||
|
||||
conversation.on("session:error", function(err) {
|
||||
expect(err.message).eql(errMsg);
|
||||
done();
|
||||
}).initiate({ client: fakeClient, outgoing: true });
|
||||
});
|
||||
|
||||
it("should trigger a `session:expired` event when errno is 105",
|
||||
function(done) {
|
||||
var err = new Error("HTTP 404 Not Found; fake");
|
||||
err.errno = 105;
|
||||
requestCallInfoStub.callsArgWith(2, err);
|
||||
|
||||
conversation.on("session:expired", function(err2) {
|
||||
expect(err2).eql(err);
|
||||
done();
|
||||
}).initiate({ client: fakeClient, outgoing: true });
|
||||
});
|
||||
conversation.outgoing();
|
||||
});
|
||||
|
||||
it("should end the session on outgoing call timeout", function() {
|
||||
requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
|
||||
|
||||
conversation.initiate({
|
||||
client: fakeClient,
|
||||
outgoing: true
|
||||
});
|
||||
conversation.outgoing();
|
||||
|
||||
sandbox.clock.tick(1001);
|
||||
|
||||
@ -194,35 +128,24 @@ describe("loop.shared.models", function() {
|
||||
|
||||
it("should trigger a `timeout` event on outgoing call timeout",
|
||||
function(done) {
|
||||
requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
|
||||
|
||||
conversation.once("timeout", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
conversation.initiate({
|
||||
client: fakeClient,
|
||||
outgoing: true
|
||||
});
|
||||
conversation.outgoing();
|
||||
|
||||
sandbox.clock.tick(1001);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setReady", function() {
|
||||
describe("#setSessionData", function() {
|
||||
it("should update conversation session information", function() {
|
||||
conversation.setReady(fakeSessionData);
|
||||
conversation.setSessionData(fakeSessionData);
|
||||
|
||||
expect(conversation.get("sessionId")).eql("sessionId");
|
||||
expect(conversation.get("sessionToken")).eql("sessionToken");
|
||||
expect(conversation.get("apiKey")).eql("apiKey");
|
||||
});
|
||||
|
||||
it("should trigger a `session:ready` event", function(done) {
|
||||
conversation.on("session:ready", function() {
|
||||
done();
|
||||
}).setReady(fakeSessionData);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#startSession", function() {
|
||||
|
@ -97,7 +97,6 @@ describe("loop.shared.router", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
TestRouter = loop.shared.router.BaseConversationRouter.extend({
|
||||
startCall: sandbox.spy(),
|
||||
endCall: sandbox.spy()
|
||||
});
|
||||
conversation = new loop.shared.models.ConversationModel({
|
||||
@ -111,9 +110,14 @@ describe("loop.shared.router", function() {
|
||||
describe("#constructor", function() {
|
||||
it("should require a ConversationModel instance", function() {
|
||||
expect(function() {
|
||||
new TestRouter();
|
||||
new TestRouter({ client: {} });
|
||||
}).to.Throw(Error, /missing required conversation/);
|
||||
});
|
||||
it("should require a Client instance", function() {
|
||||
expect(function() {
|
||||
new TestRouter({ conversation: {} });
|
||||
}).to.Throw(Error, /missing required client/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
@ -127,7 +131,8 @@ describe("loop.shared.router", function() {
|
||||
};
|
||||
router = new TestRouter({
|
||||
conversation: conversation,
|
||||
notifier: notifier
|
||||
notifier: notifier,
|
||||
client: {}
|
||||
});
|
||||
});
|
||||
|
||||
@ -149,12 +154,6 @@ describe("loop.shared.router", function() {
|
||||
|
||||
});
|
||||
|
||||
it("should call startCall() once the call session is ready", function() {
|
||||
conversation.trigger("session:ready");
|
||||
|
||||
sinon.assert.calledOnce(router.startCall);
|
||||
});
|
||||
|
||||
it("should call endCall() when conversation ended", function() {
|
||||
conversation.trigger("session:ended");
|
||||
|
||||
|
@ -17,6 +17,9 @@ describe("loop.webapp", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
// conversation#outgoing sets timers, so we need to use fake ones
|
||||
// to prevent random failures.
|
||||
sandbox.useFakeTimers();
|
||||
notifier = {
|
||||
notify: sandbox.spy(),
|
||||
warn: sandbox.spy(),
|
||||
@ -69,9 +72,13 @@ describe("loop.webapp", function() {
|
||||
});
|
||||
|
||||
describe("WebappRouter", function() {
|
||||
var router, conversation;
|
||||
var router, conversation, client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.StandaloneClient({
|
||||
baseServerUrl: "http://fake.example.com"
|
||||
});
|
||||
sandbox.stub(client, "requestCallInfo");
|
||||
conversation = new sharedModels.ConversationModel({}, {
|
||||
sdk: {},
|
||||
pendingCallTimeout: 1000
|
||||
@ -79,6 +86,7 @@ describe("loop.webapp", function() {
|
||||
sandbox.stub(loop.webapp.WebappRouter.prototype, "loadReactComponent");
|
||||
router = new loop.webapp.WebappRouter({
|
||||
helper: {},
|
||||
client: client,
|
||||
conversation: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
@ -162,14 +170,14 @@ describe("loop.webapp", function() {
|
||||
expect(conversation.get("loopToken")).eql("fakeToken");
|
||||
});
|
||||
|
||||
it("should load the ConversationFormView", function() {
|
||||
it("should load the StartConversationView", function() {
|
||||
router.initiate("fakeToken");
|
||||
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWithExactly(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return React.addons.TestUtils.isDescriptorOfType(
|
||||
value, loop.webapp.ConversationFormView);
|
||||
value, loop.webapp.StartConversationView);
|
||||
}));
|
||||
});
|
||||
|
||||
@ -242,7 +250,8 @@ describe("loop.webapp", function() {
|
||||
|
||||
it("should navigate to call/ongoing/:token once call session is ready",
|
||||
function() {
|
||||
conversation.trigger("session:ready");
|
||||
router.setupOutgoingCall();
|
||||
conversation.outgoing(fakeSessionData);
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ongoing/fakeToken");
|
||||
@ -270,17 +279,81 @@ describe("loop.webapp", function() {
|
||||
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
|
||||
});
|
||||
|
||||
it("should navigate to call/expired when a session:expired event is " +
|
||||
"received", function() {
|
||||
conversation.trigger("session:expired");
|
||||
describe("#setupOutgoingCall", function() {
|
||||
beforeEach(function() {
|
||||
router.initiate();
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "/call/expired");
|
||||
describe("No loop token", function() {
|
||||
it("should navigate to home", function() {
|
||||
conversation.setupOutgoingCall();
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "home");
|
||||
});
|
||||
|
||||
it("should display an error", function() {
|
||||
conversation.setupOutgoingCall();
|
||||
|
||||
sinon.assert.calledOnce(notifier.errorL10n);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Has loop token", function() {
|
||||
beforeEach(function() {
|
||||
conversation.set("loopToken", "fakeToken");
|
||||
sandbox.stub(conversation, "outgoing");
|
||||
});
|
||||
|
||||
it("should call requestCallInfo on the client",
|
||||
function() {
|
||||
conversation.setupOutgoingCall();
|
||||
|
||||
sinon.assert.calledOnce(client.requestCallInfo);
|
||||
sinon.assert.calledWith(client.requestCallInfo, "fakeToken",
|
||||
"audio-video");
|
||||
});
|
||||
|
||||
describe("requestCallInfo response handling", function() {
|
||||
it("should navigate to call/expired when a session has expired",
|
||||
function() {
|
||||
client.requestCallInfo.callsArgWith(2, {errno: 105});
|
||||
conversation.setupOutgoingCall();
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "/call/expired");
|
||||
});
|
||||
|
||||
it("should navigate to home on any other error", function() {
|
||||
client.requestCallInfo.callsArgWith(2, {errno: 104});
|
||||
conversation.setupOutgoingCall();
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "home");
|
||||
});
|
||||
|
||||
it("should notify the user on any other error", function() {
|
||||
client.requestCallInfo.callsArgWith(2, {errno: 104});
|
||||
conversation.setupOutgoingCall();
|
||||
|
||||
sinon.assert.calledOnce(notifier.errorL10n);
|
||||
});
|
||||
|
||||
it("should call outgoing on the conversation model when details " +
|
||||
"are successfully received", function() {
|
||||
client.requestCallInfo.callsArgWith(2, null, fakeSessionData);
|
||||
conversation.setupOutgoingCall();
|
||||
|
||||
sinon.assert.calledOnce(conversation.outgoing);
|
||||
sinon.assert.calledWithExactly(conversation.outgoing, fakeSessionData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConversationFormView", function() {
|
||||
describe("StartConversationView", function() {
|
||||
var conversation;
|
||||
|
||||
beforeEach(function() {
|
||||
@ -298,7 +371,8 @@ describe("loop.webapp", function() {
|
||||
});
|
||||
|
||||
describe("#initiate", function() {
|
||||
var conversation, initiate, view, fakeSubmitEvent, requestCallUrlInfo;
|
||||
var conversation, setupOutgoingCall, view, fakeSubmitEvent,
|
||||
requestCallUrlInfo;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new sharedModels.ConversationModel({}, {
|
||||
@ -307,17 +381,17 @@ describe("loop.webapp", function() {
|
||||
});
|
||||
|
||||
fakeSubmitEvent = {preventDefault: sinon.spy()};
|
||||
initiate = sinon.stub(conversation, "initiate");
|
||||
setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
|
||||
|
||||
var standaloneClientStub = {
|
||||
requestCallUrlInfo: function(token, cb) {
|
||||
cb(null, {urlCreationDate: 0});
|
||||
},
|
||||
settings: {baseServerUrl: loop.webapp.baseServerUrl}
|
||||
}
|
||||
};
|
||||
|
||||
view = React.addons.TestUtils.renderIntoDocument(
|
||||
loop.webapp.ConversationFormView({
|
||||
loop.webapp.StartConversationView({
|
||||
model: conversation,
|
||||
notifier: notifier,
|
||||
client: standaloneClientStub
|
||||
@ -329,11 +403,7 @@ describe("loop.webapp", function() {
|
||||
var button = view.getDOMNode().querySelector("button");
|
||||
React.addons.TestUtils.Simulate.click(button);
|
||||
|
||||
sinon.assert.calledOnce(initiate);
|
||||
sinon.assert.calledWith(initiate, sinon.match(function (value) {
|
||||
return !!value.outgoing &&
|
||||
(value.client.settings.baseServerUrl === loop.webapp.baseServerUrl)
|
||||
}, "outgoing: true && correct baseServerUrl"));
|
||||
sinon.assert.calledOnce(setupOutgoingCall);
|
||||
});
|
||||
|
||||
it("should disable current form once session is initiated", function() {
|
||||
@ -373,7 +443,7 @@ describe("loop.webapp", function() {
|
||||
requestCallUrlInfo = sandbox.stub();
|
||||
|
||||
view = React.addons.TestUtils.renderIntoDocument(
|
||||
loop.webapp.ConversationFormView({
|
||||
loop.webapp.StartConversationView({
|
||||
model: conversation,
|
||||
notifier: notifier,
|
||||
client: {requestCallUrlInfo: requestCallUrlInfo}
|
||||
|
@ -16,7 +16,7 @@
|
||||
<script src="fake-mozLoop.js"></script>
|
||||
<script src="fake-l10n.js"></script>
|
||||
<script src="../content/libs/sdk.js"></script>
|
||||
<script src="../content/shared/libs/react-0.10.0.js"></script>
|
||||
<script src="../content/shared/libs/react-0.11.1.js"></script>
|
||||
<script src="../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
|
@ -6,7 +6,7 @@ const Cu = Components.utils;
|
||||
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
|
||||
const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
|
||||
const {GetAvailableAddons, ForgetAddonsList} = require("devtools/webide/addons");
|
||||
const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
|
||||
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
|
||||
|
||||
window.addEventListener("load", function onLoad() {
|
||||
window.removeEventListener("load", onLoad);
|
||||
|
@ -5,7 +5,7 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
|
||||
%webideDTD;
|
||||
]>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
|
||||
%webideDTD;
|
||||
]>
|
||||
|
||||
|
@ -20,9 +20,3 @@ webide.jar:
|
||||
content/prefs.xhtml (prefs.xhtml)
|
||||
content/monitor.xhtml (monitor.xhtml)
|
||||
content/monitor.js (monitor.js)
|
||||
|
||||
# Temporarily include locales in content, until we're ready
|
||||
# to localize webide
|
||||
|
||||
content/webide.dtd (../locales/en-US/webide.dtd)
|
||||
content/webide.properties (../locales/en-US/webide.properties)
|
||||
|
@ -5,7 +5,7 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
|
||||
%webideDTD;
|
||||
]>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!DOCTYPE window [
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
|
||||
%webideDTD;
|
||||
]>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
|
||||
%webideDTD;
|
||||
]>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
|
||||
%webideDTD;
|
||||
]>
|
||||
|
||||
|
@ -9,7 +9,7 @@ const {AppManager} = require("devtools/webide/app-manager");
|
||||
const {Connection} = require("devtools/client/connection-manager");
|
||||
const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
|
||||
const {USBRuntime} = require("devtools/webide/runtimes");
|
||||
const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
|
||||
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
|
||||
|
||||
window.addEventListener("load", function onLoad() {
|
||||
window.removeEventListener("load", onLoad);
|
||||
|
@ -5,7 +5,7 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
|
||||
%webideDTD;
|
||||
]>
|
||||
|
||||
|
@ -22,7 +22,7 @@ const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
|
||||
const {GetAvailableAddons} = require("devtools/webide/addons");
|
||||
const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources");
|
||||
|
||||
const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
|
||||
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
|
||||
|
||||
const HTML = "http://www.w3.org/1999/xhtml";
|
||||
const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
|
||||
|
@ -5,7 +5,7 @@
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!DOCTYPE window [
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
|
||||
<!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
|
||||
%webideDTD;
|
||||
]>
|
||||
|
||||
|
@ -26,7 +26,7 @@ const {USBRuntime, WiFiRuntime, SimulatorRuntime,
|
||||
gLocalRuntime, gRemoteRuntime} = require("devtools/webide/runtimes");
|
||||
const discovery = require("devtools/toolkit/discovery/discovery");
|
||||
|
||||
const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
|
||||
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
|
||||
|
||||
const WIFI_SCANNING_PREF = "devtools.remote.wifi.scan";
|
||||
|
||||
|
@ -11,7 +11,7 @@ const {DebuggerServer} = require("resource://gre/modules/devtools/dbg-server.jsm
|
||||
const discovery = require("devtools/toolkit/discovery/discovery");
|
||||
const promise = require("promise");
|
||||
|
||||
const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
|
||||
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
|
||||
|
||||
function USBRuntime(id) {
|
||||
this.id = id;
|
||||
|
@ -63,7 +63,7 @@
|
||||
<!ENTITY projectPanel_myProjects "My Projects">
|
||||
<!ENTITY projectPanel_runtimeApps "Runtime Apps">
|
||||
<!ENTITY runtimePanel_USBDevices "USB Devices">
|
||||
<!ENTITY runtimePanel_WiFiDevices "WiFi Devices">
|
||||
<!ENTITY runtimePanel_WiFiDevices "Wi-Fi Devices">
|
||||
<!ENTITY runtimePanel_simulators "Simulators">
|
||||
<!ENTITY runtimePanel_custom "Custom">
|
||||
<!ENTITY runtimePanel_installsimulator "Install Simulator">
|
@ -22,12 +22,20 @@ importHostedApp_header=Enter Manifest URL
|
||||
notification_showTroubleShooting_label=troubleshooting
|
||||
notification_showTroubleShooting_accesskey=t
|
||||
|
||||
error_operationTimeout=Operation timed out: %1$S
|
||||
error_operationFail=Operation failed: %1$S
|
||||
error_listRunningApps=Can't get app list from device
|
||||
error_cantConnectToApp=Can't connect to app: %1$S
|
||||
# These messages appear in a notification box when an error occur.
|
||||
|
||||
error_cantInstallNotFullyConnected=Can't install project. Not fully connected.
|
||||
error_cantInstallValidationErrors=Can't install project. Validation errors.
|
||||
error_listRunningApps=Can't get app list from device
|
||||
|
||||
# Variable: name of the operation (in english)
|
||||
error_operationTimeout=Operation timed out: %1$S
|
||||
error_operationFail=Operation failed: %1$S
|
||||
|
||||
# Variable: app name
|
||||
error_cantConnectToApp=Can't connect to app: %1$S
|
||||
|
||||
# Variable: error message (in english)
|
||||
error_cantFetchAddonsJSON=Can't fetch the add-on list: %S
|
||||
|
||||
addons_stable=stable
|
@ -64,6 +64,8 @@
|
||||
locale/browser/devtools/font-inspector.dtd (%chrome/browser/devtools/font-inspector.dtd)
|
||||
locale/browser/devtools/app-manager.dtd (%chrome/browser/devtools/app-manager.dtd)
|
||||
locale/browser/devtools/app-manager.properties (%chrome/browser/devtools/app-manager.properties)
|
||||
locale/browser/devtools/webide.dtd (%chrome/browser/devtools/webide.dtd)
|
||||
locale/browser/devtools/webide.properties (%chrome/browser/devtools/webide.properties)
|
||||
locale/browser/loop/loop.properties (%chrome/browser/loop/loop.properties)
|
||||
locale/browser/newTab.dtd (%chrome/browser/newTab.dtd)
|
||||
locale/browser/newTab.properties (%chrome/browser/newTab.properties)
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.9 KiB |
@ -140,25 +140,42 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
|
||||
Header header = response.getFirstHeader("Location");
|
||||
|
||||
// Handle mad webservers.
|
||||
if (header == null) {
|
||||
return null;
|
||||
final String newURI;
|
||||
try {
|
||||
if (header == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
newURI = header.getValue();
|
||||
if (newURI == null || newURI.equals(faviconURI.toString())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (visited.contains(newURI)) {
|
||||
// Already been redirected here - abort.
|
||||
return null;
|
||||
}
|
||||
|
||||
visited.add(newURI);
|
||||
} finally {
|
||||
// Consume the entity before recurse or exit.
|
||||
try {
|
||||
response.getEntity().consumeContent();
|
||||
} catch (Exception e) {
|
||||
// Doesn't matter.
|
||||
}
|
||||
}
|
||||
|
||||
String newURI = header.getValue();
|
||||
if (newURI == null || newURI.equals(faviconURI.toString())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (visited.contains(newURI)) {
|
||||
// Already been redirected here - abort.
|
||||
return null;
|
||||
}
|
||||
|
||||
visited.add(newURI);
|
||||
return tryDownloadRecurse(new URI(newURI), visited);
|
||||
}
|
||||
|
||||
if (status >= 400) {
|
||||
// Consume the entity and exit.
|
||||
try {
|
||||
response.getEntity().consumeContent();
|
||||
} catch (Exception e) {
|
||||
// Doesn't matter.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user