Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-08-05 15:41:46 -04:00
commit e4f7ef24d3
39 changed files with 745 additions and 376 deletions

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

@ -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)})
)
)
);

View File

@ -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>
);

View File

@ -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;
});
},
/**

View File

@ -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.
*/

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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);

View File

@ -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");

View 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()

View File

@ -0,0 +1,6 @@
[DEFAULT]
b2g = false
browser = true
qemu = false
[test_get_url.py]

View 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)

View File

@ -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() {

View File

@ -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");

View File

@ -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}

View File

@ -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>

View File

@ -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);

View File

@ -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;
]>

View File

@ -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;
]>

View File

@ -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)

View File

@ -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;
]>

View File

@ -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;
]>

View File

@ -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;
]>

View File

@ -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;
]>

View File

@ -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);

View File

@ -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;
]>

View File

@ -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";

View File

@ -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;
]>

View File

@ -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";

View File

@ -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;

View File

@ -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">

View File

@ -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

View File

@ -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

View File

@ -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;
}
}