Bug 1033841: Ported Loop panel views to React. r=Standard8
This commit is contained in:
@@ -6,3 +6,16 @@ The standalone client is a set of web pages intended to be hosted on a standalon
|
|||||||
|
|
||||||
The standalone client exists in standalone/ but shares items (from content/shared/) with the desktop implementation. See the README.md file in the standalone/ directory for how to run the server locally.
|
The standalone client exists in standalone/ but shares items (from content/shared/) with the desktop implementation. See the README.md file in the standalone/ directory for how to run the server locally.
|
||||||
|
|
||||||
|
Working with JSX
|
||||||
|
================
|
||||||
|
|
||||||
|
You need to install the JSX compiler in order to compile the .jsx files into regular .js ones.
|
||||||
|
|
||||||
|
The JSX compiler is installable using npm:
|
||||||
|
|
||||||
|
npm install -g react-tools
|
||||||
|
|
||||||
|
Once installed, run it with the --watch option, eg.:
|
||||||
|
|
||||||
|
jsx --watch --x jsx browser/components/loop/content/js/src \
|
||||||
|
browser/components/loop/content/js
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
/* jshint esnext:true */
|
||||||
/* global loop:true, hawk, deriveHawkCredentials */
|
/* global loop:true, hawk, deriveHawkCredentials */
|
||||||
|
|
||||||
var loop = loop || {};
|
var loop = loop || {};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
/* jshint esnext:true */
|
||||||
/* global loop:true */
|
/* global loop:true */
|
||||||
|
|
||||||
var loop = loop || {};
|
var loop = loop || {};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
/* jshint esnext:true */
|
||||||
/* global loop:true */
|
/* global loop:true */
|
||||||
|
|
||||||
var loop = loop || {};
|
var loop = loop || {};
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
/** @jsx React.DOM */
|
||||||
|
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
/* global loop:true */
|
/*jshint newcap:false*/
|
||||||
|
/*global loop:true, React */
|
||||||
|
|
||||||
var loop = loop || {};
|
var loop = loop || {};
|
||||||
loop.panel = (function(_, mozL10n) {
|
loop.panel = (function(_, mozL10n) {
|
||||||
@@ -21,162 +24,188 @@ loop.panel = (function(_, mozL10n) {
|
|||||||
/**
|
/**
|
||||||
* Do not disturb panel subview.
|
* Do not disturb panel subview.
|
||||||
*/
|
*/
|
||||||
var DoNotDisturbView = sharedViews.BaseView.extend({
|
var DoNotDisturb = React.createClass({displayName: 'DoNotDisturb',
|
||||||
template: _.template([
|
getInitialState: function() {
|
||||||
'<label>',
|
return {doNotDisturb: navigator.mozLoop.doNotDisturb};
|
||||||
' <input type="checkbox" <%- checked %>>',
|
|
||||||
' <span data-l10n-id="do_not_disturb"></span>',
|
|
||||||
'</label>',
|
|
||||||
].join('')),
|
|
||||||
|
|
||||||
events: {
|
|
||||||
"click input[type=checkbox]": "toggle"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
handleCheckboxChange: function() {
|
||||||
* Toggles mozLoop activation status.
|
// Note: side effect!
|
||||||
*/
|
|
||||||
toggle: function() {
|
|
||||||
navigator.mozLoop.doNotDisturb = !navigator.mozLoop.doNotDisturb;
|
navigator.mozLoop.doNotDisturb = !navigator.mozLoop.doNotDisturb;
|
||||||
this.render();
|
this.setState({doNotDisturb: navigator.mozLoop.doNotDisturb});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
this.$el.html(this.template({
|
// XXX https://github.com/facebook/react/issues/310 for === htmlFor
|
||||||
checked: navigator.mozLoop.doNotDisturb ? "checked" : ""
|
return (
|
||||||
}));
|
React.DOM.p( {className:"dnd"},
|
||||||
return this;
|
React.DOM.input( {type:"checkbox", checked:this.state.doNotDisturb,
|
||||||
|
id:"dnd-component", onChange:this.handleCheckboxChange} ),
|
||||||
|
React.DOM.label( {htmlFor:"dnd-component"}, __("do_not_disturb"))
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var ToSView = sharedViews.BaseView.extend({
|
var ToSView = React.createClass({displayName: 'ToSView',
|
||||||
template: _.template([
|
getInitialState: function() {
|
||||||
'<p data-l10n-id="legal_text_and_links"',
|
return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
|
||||||
' data-l10n-args=\'',
|
},
|
||||||
' {"terms_of_use_url": "https://accounts.firefox.com/legal/terms",',
|
|
||||||
' "privacy_notice_url": "www.mozilla.org/privacy/"',
|
|
||||||
' }\'></p>'
|
|
||||||
].join('')),
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
if (navigator.mozLoop.getLoopCharPref('seenToS') === null) {
|
var tosHTML = __("legal_text_and_links", {
|
||||||
this.$el.html(this.template());
|
"terms_of_use_url": "https://accounts.firefox.com/legal/terms",
|
||||||
|
"privacy_notice_url": "www.mozilla.org/privacy/"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.state.seenToS) {
|
||||||
navigator.mozLoop.setLoopCharPref('seenToS', 'seen');
|
navigator.mozLoop.setLoopCharPref('seenToS', 'seen');
|
||||||
|
return React.DOM.p( {className:"tos",
|
||||||
|
dangerouslySetInnerHTML:{__html: tosHTML}});
|
||||||
|
} else {
|
||||||
|
return React.DOM.div(null );
|
||||||
}
|
}
|
||||||
return this;
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var PanelLayout = React.createClass({displayName: 'PanelLayout',
|
||||||
|
propTypes: {
|
||||||
|
summary: React.PropTypes.string.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
React.DOM.div( {className:"share generate-url"},
|
||||||
|
React.DOM.div( {className:"description"},
|
||||||
|
React.DOM.p(null, this.props.summary)
|
||||||
|
),
|
||||||
|
React.DOM.div( {className:"action"},
|
||||||
|
this.props.children
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var CallUrlResult = React.createClass({displayName: 'CallUrlResult',
|
||||||
|
propTypes: {
|
||||||
|
callUrl: React.PropTypes.string.isRequired,
|
||||||
|
retry: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
handleButtonClick: function() {
|
||||||
|
this.props.retry();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// XXX setting elem value from a state (in the callUrl input)
|
||||||
|
// makes it immutable ie read only but that is fine in our case.
|
||||||
|
// readOnly attr will suppress a warning regarding this issue
|
||||||
|
// from the react lib.
|
||||||
|
return (
|
||||||
|
PanelLayout( {summary:__("share_link_url")},
|
||||||
|
React.DOM.div( {className:"invite"},
|
||||||
|
React.DOM.input( {type:"url", value:this.props.callUrl, readOnly:"true"} ),
|
||||||
|
React.DOM.button( {onClick:this.handleButtonClick,
|
||||||
|
className:"btn btn-success"}, __("new_url"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var CallUrlForm = React.createClass({displayName: 'CallUrlForm',
|
||||||
|
propTypes: {
|
||||||
|
client: React.PropTypes.object.isRequired,
|
||||||
|
notifier: React.PropTypes.object.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
pending: false,
|
||||||
|
disabled: true,
|
||||||
|
callUrl: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
retry: function() {
|
||||||
|
this.setState(this.getInitialState());
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTextChange: function(event) {
|
||||||
|
this.setState({disabled: !event.currentTarget.value});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFormSubmit: function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.setState({pending: true});
|
||||||
|
|
||||||
|
this.props.client.requestCallUrl(
|
||||||
|
this.refs.caller.getDOMNode().value, this._onCallUrlReceived);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCallUrlReceived: function(err, callUrlData) {
|
||||||
|
var callUrl = false;
|
||||||
|
|
||||||
|
this.props.notifier.clear();
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
this.props.notifier.errorL10n("unable_retrieve_url");
|
||||||
|
} else {
|
||||||
|
callUrl = callUrlData.callUrl || callUrlData.call_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({pending: false, callUrl: callUrl});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// If we have a call url, render result
|
||||||
|
if (this.state.callUrl) {
|
||||||
|
return (
|
||||||
|
CallUrlResult( {callUrl:this.state.callUrl, retry:this.retry})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't display the form
|
||||||
|
var cx = React.addons.classSet;
|
||||||
|
return (
|
||||||
|
PanelLayout( {summary:__("get_link_to_share")},
|
||||||
|
React.DOM.form( {className:"invite", onSubmit:this.handleFormSubmit},
|
||||||
|
|
||||||
|
React.DOM.input( {type:"text", name:"caller", ref:"caller", required:"required",
|
||||||
|
className:cx({'pending': this.state.pending}),
|
||||||
|
onChange:this.handleTextChange,
|
||||||
|
placeholder:__("call_identifier_textinput_placeholder")} ),
|
||||||
|
|
||||||
|
React.DOM.button( {type:"submit", className:"get-url btn btn-success",
|
||||||
|
disabled:this.state.disabled},
|
||||||
|
__("get_a_call_url")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ToSView(null )
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Panel view.
|
* Panel view.
|
||||||
*/
|
*/
|
||||||
var PanelView = sharedViews.BaseView.extend({
|
var PanelView = React.createClass({displayName: 'PanelView',
|
||||||
template: _.template([
|
propTypes: {
|
||||||
'<div class="description">',
|
notifier: React.PropTypes.object.isRequired,
|
||||||
' <p data-l10n-id="get_link_to_share"></p>',
|
client: React.PropTypes.object.isRequired
|
||||||
'</div>',
|
|
||||||
'<div class="action">',
|
|
||||||
' <form class="invite">',
|
|
||||||
' <input type="text" name="caller" data-l10n-id="caller" required>',
|
|
||||||
' <button type="submit" class="get-url btn btn-success"',
|
|
||||||
' data-l10n-id="get_a_call_url"></button>',
|
|
||||||
' </form>',
|
|
||||||
' <p class="tos"></p>',
|
|
||||||
' <p class="result hide">',
|
|
||||||
' <input id="call-url" type="url" readonly>',
|
|
||||||
' <a class="go-back btn btn-info" href="" data-l10n-id="new_url"></a>',
|
|
||||||
' </p>',
|
|
||||||
' <p class="dnd"></p>',
|
|
||||||
'</div>',
|
|
||||||
].join("")),
|
|
||||||
|
|
||||||
className: "share generate-url",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do not disturb view.
|
|
||||||
* @type {DoNotDisturbView|undefined}
|
|
||||||
*/
|
|
||||||
dndView: undefined,
|
|
||||||
|
|
||||||
events: {
|
|
||||||
"keyup input[name=caller]": "changeButtonState",
|
|
||||||
"submit form.invite": "getCallUrl",
|
|
||||||
"click a.go-back": "goBack"
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function(options) {
|
|
||||||
options = options || {};
|
|
||||||
if (!options.notifier) {
|
|
||||||
throw new Error("missing required notifier");
|
|
||||||
}
|
|
||||||
this.notifier = options.notifier;
|
|
||||||
this.client = new loop.Client();
|
|
||||||
},
|
|
||||||
|
|
||||||
getNickname: function() {
|
|
||||||
return this.$("input[name=caller]").val();
|
|
||||||
},
|
|
||||||
|
|
||||||
getCallUrl: function(event) {
|
|
||||||
this.notifier.clear();
|
|
||||||
event.preventDefault();
|
|
||||||
var callback = function(err, callUrlData) {
|
|
||||||
this.clearPending();
|
|
||||||
if (err) {
|
|
||||||
this.notifier.errorL10n("unable_retrieve_url");
|
|
||||||
this.render();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.onCallUrlReceived(callUrlData);
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
this.setPending();
|
|
||||||
this.client.requestCallUrl(this.getNickname(), callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
goBack: function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.$(".action .result").hide();
|
|
||||||
this.$(".action .invite").show();
|
|
||||||
this.$(".description p").text(__("get_link_to_share"));
|
|
||||||
this.changeButtonState();
|
|
||||||
},
|
|
||||||
|
|
||||||
onCallUrlReceived: function(callUrlData) {
|
|
||||||
this.notifier.clear();
|
|
||||||
this.$(".action .invite").hide();
|
|
||||||
this.$(".action .invite input").val("");
|
|
||||||
this.$(".action .result input").val(callUrlData.callUrl);
|
|
||||||
this.$(".action .result").show();
|
|
||||||
this.$(".description p").text(__("share_link_url"));
|
|
||||||
},
|
|
||||||
|
|
||||||
setPending: function() {
|
|
||||||
this.$("[name=caller]").addClass("pending");
|
|
||||||
this.$(".get-url").addClass("disabled").attr("disabled", "disabled");
|
|
||||||
},
|
|
||||||
|
|
||||||
clearPending: function() {
|
|
||||||
this.$("[name=caller]").removeClass("pending");
|
|
||||||
this.changeButtonState();
|
|
||||||
},
|
|
||||||
|
|
||||||
changeButtonState: function() {
|
|
||||||
var enabled = !!this.$("input[name=caller]").val();
|
|
||||||
if (enabled) {
|
|
||||||
this.$(".get-url").removeClass("disabled")
|
|
||||||
.removeAttr("disabled", "disabled");
|
|
||||||
} else {
|
|
||||||
this.$(".get-url").addClass("disabled").attr("disabled", "disabled");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
this.$el.html(this.template());
|
return (
|
||||||
// Do not Disturb sub view
|
React.DOM.div(null,
|
||||||
this.dndView = new DoNotDisturbView({el: this.$(".dnd")}).render();
|
CallUrlForm( {client:this.props.client,
|
||||||
this.tosView = new ToSView({el: this.$(".tos")}).render();
|
notifier:this.props.notifier} ),
|
||||||
return this;
|
DoNotDisturb(null )
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -230,10 +259,12 @@ loop.panel = (function(_, mozL10n) {
|
|||||||
* Resets this router to its initial state.
|
* Resets this router to its initial state.
|
||||||
*/
|
*/
|
||||||
reset: function() {
|
reset: function() {
|
||||||
// purge pending notifications
|
|
||||||
this._notifier.clear();
|
this._notifier.clear();
|
||||||
// reset home view
|
var client = new loop.Client({
|
||||||
this.loadView(new PanelView({notifier: this._notifier}));
|
baseServerUrl: navigator.mozLoop.serverUrl
|
||||||
|
});
|
||||||
|
this.loadReactComponent(PanelView( {client:client,
|
||||||
|
notifier:this._notifier} ));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -259,8 +290,9 @@ loop.panel = (function(_, mozL10n) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
init: init,
|
init: init,
|
||||||
|
DoNotDisturb: DoNotDisturb,
|
||||||
|
CallUrlForm: CallUrlForm,
|
||||||
PanelView: PanelView,
|
PanelView: PanelView,
|
||||||
DoNotDisturbView: DoNotDisturbView,
|
|
||||||
PanelRouter: PanelRouter,
|
PanelRouter: PanelRouter,
|
||||||
ToSView: ToSView
|
ToSView: ToSView
|
||||||
};
|
};
|
||||||
|
|||||||
299
browser/components/loop/content/js/src/panel.jsx
Normal file
299
browser/components/loop/content/js/src/panel.jsx
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
/** @jsx React.DOM */
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
/*jshint newcap:false*/
|
||||||
|
/*global loop:true, React */
|
||||||
|
|
||||||
|
var loop = loop || {};
|
||||||
|
loop.panel = (function(_, mozL10n) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var sharedViews = loop.shared.views,
|
||||||
|
// aliasing translation function as __ for concision
|
||||||
|
__ = mozL10n.get;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel router.
|
||||||
|
* @type {loop.desktopRouter.DesktopRouter}
|
||||||
|
*/
|
||||||
|
var router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not disturb panel subview.
|
||||||
|
*/
|
||||||
|
var DoNotDisturb = React.createClass({
|
||||||
|
getInitialState: function() {
|
||||||
|
return {doNotDisturb: navigator.mozLoop.doNotDisturb};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCheckboxChange: function() {
|
||||||
|
// Note: side effect!
|
||||||
|
navigator.mozLoop.doNotDisturb = !navigator.mozLoop.doNotDisturb;
|
||||||
|
this.setState({doNotDisturb: navigator.mozLoop.doNotDisturb});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// XXX https://github.com/facebook/react/issues/310 for === htmlFor
|
||||||
|
return (
|
||||||
|
<p className="dnd">
|
||||||
|
<input type="checkbox" checked={this.state.doNotDisturb}
|
||||||
|
id="dnd-component" onChange={this.handleCheckboxChange} />
|
||||||
|
<label htmlFor="dnd-component">{__("do_not_disturb")}</label>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var ToSView = React.createClass({
|
||||||
|
getInitialState: function() {
|
||||||
|
return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var tosHTML = __("legal_text_and_links", {
|
||||||
|
"terms_of_use_url": "https://accounts.firefox.com/legal/terms",
|
||||||
|
"privacy_notice_url": "www.mozilla.org/privacy/"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.state.seenToS) {
|
||||||
|
navigator.mozLoop.setLoopCharPref('seenToS', 'seen');
|
||||||
|
return <p className="tos"
|
||||||
|
dangerouslySetInnerHTML={{__html: tosHTML}}></p>;
|
||||||
|
} else {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var PanelLayout = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
summary: React.PropTypes.string.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div className="share generate-url">
|
||||||
|
<div className="description">
|
||||||
|
<p>{this.props.summary}</p>
|
||||||
|
</div>
|
||||||
|
<div className="action">
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var CallUrlResult = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
callUrl: React.PropTypes.string.isRequired,
|
||||||
|
retry: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
handleButtonClick: function() {
|
||||||
|
this.props.retry();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// XXX setting elem value from a state (in the callUrl input)
|
||||||
|
// makes it immutable ie read only but that is fine in our case.
|
||||||
|
// readOnly attr will suppress a warning regarding this issue
|
||||||
|
// from the react lib.
|
||||||
|
return (
|
||||||
|
<PanelLayout summary={__("share_link_url")}>
|
||||||
|
<div className="invite">
|
||||||
|
<input type="url" value={this.props.callUrl} readOnly="true" />
|
||||||
|
<button onClick={this.handleButtonClick}
|
||||||
|
className="btn btn-success">{__("new_url")}</button>
|
||||||
|
</div>
|
||||||
|
</PanelLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var CallUrlForm = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
client: React.PropTypes.object.isRequired,
|
||||||
|
notifier: React.PropTypes.object.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
pending: false,
|
||||||
|
disabled: true,
|
||||||
|
callUrl: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
retry: function() {
|
||||||
|
this.setState(this.getInitialState());
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTextChange: function(event) {
|
||||||
|
this.setState({disabled: !event.currentTarget.value});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFormSubmit: function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.setState({pending: true});
|
||||||
|
|
||||||
|
this.props.client.requestCallUrl(
|
||||||
|
this.refs.caller.getDOMNode().value, this._onCallUrlReceived);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCallUrlReceived: function(err, callUrlData) {
|
||||||
|
var callUrl = false;
|
||||||
|
|
||||||
|
this.props.notifier.clear();
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
this.props.notifier.errorL10n("unable_retrieve_url");
|
||||||
|
} else {
|
||||||
|
callUrl = callUrlData.callUrl || callUrlData.call_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({pending: false, callUrl: callUrl});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// If we have a call url, render result
|
||||||
|
if (this.state.callUrl) {
|
||||||
|
return (
|
||||||
|
<CallUrlResult callUrl={this.state.callUrl} retry={this.retry}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't display the form
|
||||||
|
var cx = React.addons.classSet;
|
||||||
|
return (
|
||||||
|
<PanelLayout summary={__("get_link_to_share")}>
|
||||||
|
<form className="invite" onSubmit={this.handleFormSubmit}>
|
||||||
|
|
||||||
|
<input type="text" name="caller" ref="caller" required="required"
|
||||||
|
className={cx({'pending': this.state.pending})}
|
||||||
|
onChange={this.handleTextChange}
|
||||||
|
placeholder={__("call_identifier_textinput_placeholder")} />
|
||||||
|
|
||||||
|
<button type="submit" className="get-url btn btn-success"
|
||||||
|
disabled={this.state.disabled}>
|
||||||
|
{__("get_a_call_url")}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<ToSView />
|
||||||
|
</PanelLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel view.
|
||||||
|
*/
|
||||||
|
var PanelView = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
notifier: React.PropTypes.object.isRequired,
|
||||||
|
client: React.PropTypes.object.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CallUrlForm client={this.props.client}
|
||||||
|
notifier={this.props.notifier} />
|
||||||
|
<DoNotDisturb />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
|
||||||
|
/**
|
||||||
|
* DOM document object.
|
||||||
|
* @type {HTMLDocument}
|
||||||
|
*/
|
||||||
|
document: undefined,
|
||||||
|
|
||||||
|
routes: {
|
||||||
|
"": "home"
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function(options) {
|
||||||
|
options = options || {};
|
||||||
|
if (!options.document) {
|
||||||
|
throw new Error("missing required document");
|
||||||
|
}
|
||||||
|
this.document = options.document;
|
||||||
|
|
||||||
|
this._registerVisibilityChangeEvent();
|
||||||
|
|
||||||
|
this.on("panel:open panel:closed", this.reset, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the DOM visibility API event for the whole document, and trigger
|
||||||
|
* appropriate events accordingly:
|
||||||
|
*
|
||||||
|
* - `panel:opened` when the panel is open
|
||||||
|
* - `panel:closed` when the panel is closed
|
||||||
|
*
|
||||||
|
* @link http://www.w3.org/TR/page-visibility/
|
||||||
|
*/
|
||||||
|
_registerVisibilityChangeEvent: function() {
|
||||||
|
this.document.addEventListener("visibilitychange", function(event) {
|
||||||
|
this.trigger(event.currentTarget.hidden ? "panel:closed"
|
||||||
|
: "panel:open");
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default entry point.
|
||||||
|
*/
|
||||||
|
home: function() {
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets this router to its initial state.
|
||||||
|
*/
|
||||||
|
reset: function() {
|
||||||
|
this._notifier.clear();
|
||||||
|
var client = new loop.Client({
|
||||||
|
baseServerUrl: navigator.mozLoop.serverUrl
|
||||||
|
});
|
||||||
|
this.loadReactComponent(<PanelView client={client}
|
||||||
|
notifier={this._notifier} />);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel initialisation.
|
||||||
|
*/
|
||||||
|
function init() {
|
||||||
|
// Do the initial L10n setup, we do this before anything
|
||||||
|
// else to ensure the L10n environment is setup correctly.
|
||||||
|
mozL10n.initialize(navigator.mozLoop);
|
||||||
|
|
||||||
|
router = new PanelRouter({
|
||||||
|
document: document,
|
||||||
|
notifier: new sharedViews.NotificationListView({el: "#messages"})
|
||||||
|
});
|
||||||
|
Backbone.history.start();
|
||||||
|
|
||||||
|
// Notify the window that we've finished initalization and initial layout
|
||||||
|
var evtObject = document.createEvent('Event');
|
||||||
|
evtObject.initEvent('loopPanelInitialized', true, false);
|
||||||
|
window.dispatchEvent(evtObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init,
|
||||||
|
DoNotDisturb: DoNotDisturb,
|
||||||
|
CallUrlForm: CallUrlForm,
|
||||||
|
PanelView: PanelView,
|
||||||
|
PanelRouter: PanelRouter,
|
||||||
|
ToSView: ToSView
|
||||||
|
};
|
||||||
|
})(_, document.mozL10n);
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
<div id="main"></div>
|
<div id="main"></div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="loop/shared/libs/react-0.10.0.js"></script>
|
||||||
<script type="text/javascript" src="loop/libs/l10n.js"></script>
|
<script type="text/javascript" src="loop/libs/l10n.js"></script>
|
||||||
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
||||||
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
||||||
|
|||||||
@@ -53,8 +53,11 @@ a {
|
|||||||
margin: 0 0 1em 0;
|
margin: 0 0 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share .action p.dnd {
|
p.dnd {
|
||||||
margin-top: 1em;
|
margin: 0 10px 10px 10px;
|
||||||
|
/* The panel won't increase its height when using a bottom margin, while it
|
||||||
|
works using a padding */
|
||||||
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share .action input[type="text"],
|
.share .action input[type="text"],
|
||||||
@@ -65,6 +68,7 @@ a {
|
|||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
width: 65%;
|
width: 65%;
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
|
margin-right: .35em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share .action input.pending {
|
.share .action input.pending {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ loop.shared.router = (function(l10n) {
|
|||||||
var BaseRouter = Backbone.Router.extend({
|
var BaseRouter = Backbone.Router.extend({
|
||||||
/**
|
/**
|
||||||
* Active view.
|
* Active view.
|
||||||
* @type {loop.shared.views.BaseView}
|
* @type {Object}
|
||||||
*/
|
*/
|
||||||
_activeView: undefined,
|
_activeView: undefined,
|
||||||
|
|
||||||
@@ -52,11 +52,37 @@ loop.shared.router = (function(l10n) {
|
|||||||
* @param {loop.shared.views.BaseView} view View.
|
* @param {loop.shared.views.BaseView} view View.
|
||||||
*/
|
*/
|
||||||
loadView: function(view) {
|
loadView: function(view) {
|
||||||
if (this._activeView) {
|
this.clearActiveView();
|
||||||
this._activeView.remove();
|
this._activeView = {type: "backbone", view: view.render().show()};
|
||||||
|
this.updateView(this._activeView.view.$el);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a React component as current active view.
|
||||||
|
*
|
||||||
|
* @param {React} reactComponent React component.
|
||||||
|
*/
|
||||||
|
loadReactComponent: function(reactComponent) {
|
||||||
|
this.clearActiveView();
|
||||||
|
this._activeView = {
|
||||||
|
type: "react",
|
||||||
|
view: React.renderComponent(reactComponent,
|
||||||
|
document.querySelector("#main"))
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears current active view.
|
||||||
|
*/
|
||||||
|
clearActiveView: function() {
|
||||||
|
if (!this._activeView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._activeView.type === "react") {
|
||||||
|
React.unmountComponentAtNode(document.querySelector("#main"));
|
||||||
|
} else {
|
||||||
|
this._activeView.view.remove();
|
||||||
}
|
}
|
||||||
this._activeView = view.render().show();
|
|
||||||
this.updateView(this._activeView.$el);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -139,8 +139,11 @@ describe("loop.conversation", function() {
|
|||||||
router.accept();
|
router.accept();
|
||||||
|
|
||||||
sinon.assert.calledOnce(conversation.initiate);
|
sinon.assert.calledOnce(conversation.initiate);
|
||||||
sinon.assert.calledWithExactly(conversation.initiate, {
|
sinon.assert.calledWithMatch(conversation.initiate, {
|
||||||
baseServerUrl: "http://example.com",
|
client: {
|
||||||
|
mozLoop: navigator.mozLoop,
|
||||||
|
settings: {}
|
||||||
|
},
|
||||||
outgoing: false
|
outgoing: false
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<div id="fixtures"></div>
|
<div id="fixtures"></div>
|
||||||
<!-- libs -->
|
<!-- libs -->
|
||||||
<script src="../../content/libs/l10n.js"></script>
|
<script src="../../content/libs/l10n.js"></script>
|
||||||
|
<script src="../../content/shared/libs/react-0.10.0.js"></script>
|
||||||
<script src="../../content/shared/libs/jquery-2.1.0.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/lodash-2.4.1.js"></script>
|
||||||
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
||||||
@@ -35,8 +36,8 @@
|
|||||||
<script src="../../content/shared/js/router.js"></script>
|
<script src="../../content/shared/js/router.js"></script>
|
||||||
<script src="../../content/shared/js/views.js"></script>
|
<script src="../../content/shared/js/views.js"></script>
|
||||||
<script src="../../content/js/client.js"></script>
|
<script src="../../content/js/client.js"></script>
|
||||||
<script src="../../content/js/conversation.js"></script>
|
|
||||||
<script src="../../content/js/desktopRouter.js"></script>
|
<script src="../../content/js/desktopRouter.js"></script>
|
||||||
|
<script src="../../content/js/conversation.js"></script>
|
||||||
<script src="../../content/js/panel.js"></script>
|
<script src="../../content/js/panel.js"></script>
|
||||||
|
|
||||||
<!-- Test scripts -->
|
<!-- Test scripts -->
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
/*global loop, sinon */
|
/*global loop, sinon */
|
||||||
|
|
||||||
var expect = chai.expect;
|
var expect = chai.expect;
|
||||||
|
var TestUtils = React.addons.TestUtils;
|
||||||
|
|
||||||
describe("loop.panel", function() {
|
describe("loop.panel", function() {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var sandbox, notifier, fakeXHR, requests = [], savedMozLoop,
|
var sandbox, notifier, fakeXHR, requests = [];
|
||||||
fakeSeenToSPref = 0;
|
|
||||||
|
|
||||||
function createTestRouter(fakeDocument) {
|
function createTestRouter(fakeDocument) {
|
||||||
return new loop.panel.PanelRouter({
|
return new loop.panel.PanelRouter({
|
||||||
@@ -42,18 +42,13 @@ describe("loop.panel", function() {
|
|||||||
return "http://example.com";
|
return "http://example.com";
|
||||||
},
|
},
|
||||||
getStrings: function() {
|
getStrings: function() {
|
||||||
return "{}";
|
return JSON.stringify({textContent: "fakeText"});
|
||||||
},
|
},
|
||||||
get locale() {
|
get locale() {
|
||||||
return "en-US";
|
return "en-US";
|
||||||
},
|
},
|
||||||
setLoopCharPref: sandbox.stub(),
|
setLoopCharPref: sandbox.stub(),
|
||||||
getLoopCharPref: function () {
|
getLoopCharPref: sandbox.stub()
|
||||||
if (fakeSeenToSPref === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return 'seen';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.mozL10n.initialize(navigator.mozLoop);
|
document.mozL10n.initialize(navigator.mozLoop);
|
||||||
@@ -61,7 +56,6 @@ describe("loop.panel", function() {
|
|||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
delete navigator.mozLoop;
|
delete navigator.mozLoop;
|
||||||
$("#fixtures").empty();
|
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,6 +84,7 @@ describe("loop.panel", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
sandbox.stub(router, "loadView");
|
sandbox.stub(router, "loadView");
|
||||||
|
sandbox.stub(router, "loadReactComponent");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#home", function() {
|
describe("#home", function() {
|
||||||
@@ -112,13 +107,21 @@ describe("loop.panel", function() {
|
|||||||
it("should load the home view", function() {
|
it("should load the home view", function() {
|
||||||
router.reset();
|
router.reset();
|
||||||
|
|
||||||
sinon.assert.calledOnce(router.loadView);
|
sinon.assert.calledOnce(router.loadReactComponent);
|
||||||
sinon.assert.calledWithExactly(router.loadView,
|
sinon.assert.calledWithExactly(router.loadReactComponent,
|
||||||
sinon.match.instanceOf(loop.panel.PanelView));
|
sinon.match(function(value) {
|
||||||
|
return React.addons.TestUtils.isComponentOfType(
|
||||||
|
value, loop.panel.PanelView);
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Events", function() {
|
describe("Events", function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
sandbox.stub(loop.panel.PanelRouter.prototype, "trigger");
|
||||||
|
});
|
||||||
|
|
||||||
it("should listen to document visibility changes", function() {
|
it("should listen to document visibility changes", function() {
|
||||||
var fakeDocument = {
|
var fakeDocument = {
|
||||||
hidden: true,
|
hidden: true,
|
||||||
@@ -133,256 +136,204 @@ describe("loop.panel", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should trigger panel:open when the panel document is visible",
|
it("should trigger panel:open when the panel document is visible",
|
||||||
function(done) {
|
function() {
|
||||||
var router = createTestRouter({
|
var router = createTestRouter({
|
||||||
hidden: false,
|
hidden: false,
|
||||||
addEventListener: function(name, cb) {
|
addEventListener: function(name, cb) {
|
||||||
setTimeout(function() {
|
|
||||||
cb({currentTarget: {hidden: false}});
|
cb({currentTarget: {hidden: false}});
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.once("panel:open", function() {
|
sinon.assert.calledOnce(router.trigger);
|
||||||
done();
|
sinon.assert.calledWithExactly(router.trigger, "panel:open");
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should trigger panel:closed when the panel document is hidden",
|
it("should trigger panel:closed when the panel document is hidden",
|
||||||
function(done) {
|
function() {
|
||||||
var router = createTestRouter({
|
var router = createTestRouter({
|
||||||
addEventListener: function(name, cb) {
|
|
||||||
hidden: true,
|
hidden: true,
|
||||||
setTimeout(function() {
|
addEventListener: function(name, cb) {
|
||||||
cb({currentTarget: {hidden: true}});
|
cb({currentTarget: {hidden: true}});
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.once("panel:closed", function() {
|
sinon.assert.calledOnce(router.trigger);
|
||||||
done();
|
sinon.assert.calledWithExactly(router.trigger, "panel:closed");
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("loop.panel.DoNotDisturbView", function() {
|
describe("loop.panel.DoNotDisturb", function() {
|
||||||
var view;
|
var view;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
$("#fixtures").append('<div id="dnd-view"></div>');
|
view = TestUtils.renderIntoDocument(loop.panel.DoNotDisturb());
|
||||||
view = new loop.panel.DoNotDisturbView({el: $("#dnd-view")});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#toggle", function() {
|
describe("Checkbox change event", function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
navigator.mozLoop.doNotDisturb = false;
|
navigator.mozLoop.doNotDisturb = false;
|
||||||
|
|
||||||
|
var checkbox = TestUtils.findRenderedDOMComponentWithTag(view, "input");
|
||||||
|
TestUtils.Simulate.change(checkbox);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should toggle the value of mozLoop.doNotDisturb", function() {
|
it("should toggle the value of mozLoop.doNotDisturb", function() {
|
||||||
view.toggle();
|
|
||||||
|
|
||||||
expect(navigator.mozLoop.doNotDisturb).eql(true);
|
expect(navigator.mozLoop.doNotDisturb).eql(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update the DnD checkbox value", function() {
|
it("should update the DnD checkbox value", function() {
|
||||||
view.toggle();
|
expect(view.getDOMNode().querySelector("input").checked).eql(true);
|
||||||
|
|
||||||
expect(view.$("input").is(":checked")).eql(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("render", function() {
|
|
||||||
it("should check the dnd checkbox when dnd is enabled", function() {
|
|
||||||
navigator.mozLoop.doNotDisturb = false;
|
|
||||||
|
|
||||||
view.render();
|
|
||||||
|
|
||||||
expect(view.$("input").is(":checked")).eql(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should uncheck the dnd checkbox when dnd is disabled", function() {
|
|
||||||
navigator.mozLoop.doNotDisturb = true;
|
|
||||||
|
|
||||||
view.render();
|
|
||||||
|
|
||||||
expect(view.$("input").is(":checked")).eql(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("loop.panel.PanelView", function() {
|
describe("loop.panel.CallUrlForm", function() {
|
||||||
|
var fakeClient, callUrlData, view;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
$("#fixtures").append('<div id="messages"></div><div id="main"></div>');
|
callUrlData = {
|
||||||
|
call_url: "http://call.invalid/",
|
||||||
|
expiresAt: 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
fakeClient = {
|
||||||
|
requestCallUrl: function(_, cb) {
|
||||||
|
cb(null, callUrlData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
view = TestUtils.renderIntoDocument(loop.panel.CallUrlForm({
|
||||||
|
notifier: notifier,
|
||||||
|
client: fakeClient
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#getCallUrl", function() {
|
describe("#render", function() {
|
||||||
|
it("should render a ToSView", function() {
|
||||||
|
TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Form submit event", function() {
|
||||||
|
|
||||||
|
function submitForm(callerValue) {
|
||||||
|
// fill caller field
|
||||||
|
TestUtils.Simulate.change(
|
||||||
|
TestUtils.findRenderedDOMComponentWithTag(view, "input"), {
|
||||||
|
target: {value: callerValue}
|
||||||
|
});
|
||||||
|
|
||||||
|
// submit form
|
||||||
|
TestUtils.Simulate.submit(
|
||||||
|
TestUtils.findRenderedDOMComponentWithTag(view, "form"));
|
||||||
|
}
|
||||||
|
|
||||||
it("should reset all pending notifications", function() {
|
it("should reset all pending notifications", function() {
|
||||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
submitForm("foo");
|
||||||
"requestCallUrl");
|
|
||||||
var view = new loop.panel.PanelView({notifier: notifier}).render();
|
|
||||||
|
|
||||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
sinon.assert.calledOnce(notifier.clear, "clear");
|
||||||
|
|
||||||
sinon.assert.calledOnce(view.notifier.clear, "clear");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should request a call url to the server", function() {
|
it("should request a call url to the server", function() {
|
||||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
fakeClient.requestCallUrl = sandbox.stub();
|
||||||
"requestCallUrl");
|
|
||||||
var view = new loop.panel.PanelView({notifier: notifier});
|
|
||||||
sandbox.stub(view, "getNickname").returns("foo");
|
|
||||||
|
|
||||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
submitForm("foo");
|
||||||
|
|
||||||
sinon.assert.calledOnce(requestCallUrl);
|
sinon.assert.calledOnce(fakeClient.requestCallUrl);
|
||||||
sinon.assert.calledWith(requestCallUrl, "foo");
|
sinon.assert.calledWith(fakeClient.requestCallUrl, "foo");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set the call url form in a pending state", function() {
|
it("should set the call url form in a pending state", function() {
|
||||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
// Cancel requestCallUrl effect to keep the state pending
|
||||||
"requestCallUrl");
|
fakeClient.requestCallUrl = sandbox.stub();
|
||||||
sandbox.stub(loop.panel.PanelView.prototype, "setPending");
|
|
||||||
|
|
||||||
var view = new loop.panel.PanelView({notifier: notifier});
|
submitForm("foo");
|
||||||
|
|
||||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
expect(view.state.pending).eql(true);
|
||||||
|
});
|
||||||
|
|
||||||
sinon.assert.calledOnce(view.setPending);
|
it("should update state with the call url received", function() {
|
||||||
|
submitForm("foo");
|
||||||
|
|
||||||
|
expect(view.state.pending).eql(false);
|
||||||
|
expect(view.state.callUrl).eql(callUrlData.call_url);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clear the pending state when a response is received",
|
it("should clear the pending state when a response is received",
|
||||||
function() {
|
function() {
|
||||||
sandbox.stub(loop.panel.PanelView.prototype,
|
submitForm("foo");
|
||||||
"clearPending");
|
|
||||||
var requestCallUrl = sandbox.stub(
|
|
||||||
loop.Client.prototype, "requestCallUrl", function(_, cb) {
|
|
||||||
cb("fake error");
|
|
||||||
});
|
|
||||||
var view = new loop.panel.PanelView({notifier: notifier});
|
|
||||||
|
|
||||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
expect(view.state.pending).eql(false);
|
||||||
|
|
||||||
sinon.assert.calledOnce(view.clearPending);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should notify the user when the operation failed", function() {
|
it("should update CallUrlResult with the call url", function() {
|
||||||
var requestCallUrl = sandbox.stub(
|
submitForm("foo");
|
||||||
loop.Client.prototype, "requestCallUrl", function(_, cb) {
|
|
||||||
cb("fake error");
|
|
||||||
});
|
|
||||||
var view = new loop.panel.PanelView({notifier: notifier});
|
|
||||||
|
|
||||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
var urlField = view.getDOMNode().querySelector("input[type='url']");
|
||||||
|
|
||||||
sinon.assert.calledOnce(view.notifier.errorL10n);
|
expect(urlField.value).eql(callUrlData.call_url);
|
||||||
sinon.assert.calledWithExactly(view.notifier.errorL10n,
|
|
||||||
"unable_retrieve_url");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("#onCallUrlReceived", function() {
|
|
||||||
var callUrlData;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
callUrlData = {
|
|
||||||
callUrl: "http://call.me/",
|
|
||||||
expiresAt: 1000
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update the text field with the call url", function() {
|
|
||||||
var view = new loop.panel.PanelView({notifier: notifier});
|
|
||||||
view.render();
|
|
||||||
|
|
||||||
view.onCallUrlReceived(callUrlData);
|
|
||||||
|
|
||||||
expect(view.$("#call-url").val()).eql("http://call.me/");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reset all pending notifications", function() {
|
it("should reset all pending notifications", function() {
|
||||||
var view = new loop.panel.PanelView({notifier: notifier}).render();
|
submitForm("foo");
|
||||||
|
|
||||||
view.onCallUrlReceived(callUrlData);
|
sinon.assert.calledOnce(view.props.notifier.clear);
|
||||||
|
|
||||||
sinon.assert.calledOnce(view.notifier.clear);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("events", function() {
|
it("should notify the user when the operation failed", function() {
|
||||||
describe("goBack", function() {
|
fakeClient.requestCallUrl = function(_, cb) {
|
||||||
it("should update the button state");
|
cb("fake error");
|
||||||
|
};
|
||||||
|
|
||||||
|
submitForm("foo");
|
||||||
|
|
||||||
|
sinon.assert.calledOnce(notifier.errorL10n);
|
||||||
|
sinon.assert.calledWithExactly(notifier.errorL10n,
|
||||||
|
"unable_retrieve_url");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("changeButtonState", function() {
|
|
||||||
it("should do set the disabled state if there is no text");
|
|
||||||
it("should do set the enabled state if there is text");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("#render", function() {
|
|
||||||
it("should render a DoNotDisturbView", function() {
|
|
||||||
var renderDnD = sandbox.stub(loop.panel.DoNotDisturbView.prototype,
|
|
||||||
"render");
|
|
||||||
var view = new loop.panel.PanelView({notifier: notifier});
|
|
||||||
|
|
||||||
view.render();
|
|
||||||
|
|
||||||
sinon.assert.calledOnce(renderDnD);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render a ToSView", function() {
|
|
||||||
var renderToS = sandbox.stub(loop.panel.ToSView.prototype, "render");
|
|
||||||
var view = new loop.panel.PanelView({notifier: notifier});
|
|
||||||
|
|
||||||
view.render();
|
|
||||||
|
|
||||||
sinon.assert.calledOnce(renderToS);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loop.panel.ToSView', function() {
|
describe('loop.panel.ToSView', function() {
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
|
|
||||||
$('#fixtures').append('<div id="#tos-view"></div>');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// XXX Until it's possible to easily test creation of text,
|
|
||||||
// not doing so. As it stands, the magic in the L10nView
|
|
||||||
// class makes stubbing BaseView.render impractical.
|
|
||||||
|
|
||||||
it("should set the value of the loop.seenToS preference to 'seen'",
|
it("should set the value of the loop.seenToS preference to 'seen'",
|
||||||
function() {
|
function() {
|
||||||
var ToSView = new loop.panel.ToSView({el: $("#tos-view")});
|
TestUtils.renderIntoDocument(loop.panel.ToSView());
|
||||||
|
|
||||||
ToSView.render();
|
|
||||||
|
|
||||||
sinon.assert.calledOnce(navigator.mozLoop.setLoopCharPref);
|
sinon.assert.calledOnce(navigator.mozLoop.setLoopCharPref);
|
||||||
sinon.assert.calledWithExactly(navigator.mozLoop.setLoopCharPref,
|
sinon.assert.calledWithExactly(navigator.mozLoop.setLoopCharPref,
|
||||||
'seenToS', 'seen');
|
'seenToS', 'seen');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render when the value of loop.seenToS is not set", function() {
|
it("should not set the value of loop.seenToS when it's already set",
|
||||||
var renderToS = sandbox.spy(loop.panel.ToSView.prototype, "render");
|
|
||||||
var ToSView = new loop.panel.ToSView({el: $('#tos-view')});
|
|
||||||
|
|
||||||
ToSView.render();
|
|
||||||
|
|
||||||
sinon.assert.calledOnce(renderToS);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not render when the value of loop.seenToS is set to 'seen'",
|
|
||||||
function() {
|
function() {
|
||||||
var ToSView = new loop.panel.ToSView({el: $('#tos-view')});
|
navigator.mozLoop.getLoopCharPref = function() {
|
||||||
fakeSeenToSPref = 1;
|
return "seen";
|
||||||
|
};
|
||||||
|
|
||||||
ToSView.render();
|
TestUtils.renderIntoDocument(loop.panel.ToSView());
|
||||||
|
|
||||||
sinon.assert.notCalled(navigator.mozLoop.setLoopCharPref);
|
sinon.assert.notCalled(navigator.mozLoop.setLoopCharPref);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should render when the value of loop.seenToS is not set", function() {
|
||||||
|
var view = TestUtils.renderIntoDocument(loop.panel.ToSView());
|
||||||
|
|
||||||
|
TestUtils.findRenderedDOMComponentWithClass(view, "tos");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render when the value of loop.seenToS is set to 'seen'",
|
||||||
|
function(done) {
|
||||||
|
navigator.mozLoop.getLoopCharPref = function() {
|
||||||
|
return "seen";
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
TestUtils.findRenderedDOMComponentWithClass(view, "tos");
|
||||||
|
} catch (err) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ describe("loop.shared.router", function() {
|
|||||||
it("should set the active view", function() {
|
it("should set the active view", function() {
|
||||||
router.loadView(view);
|
router.loadView(view);
|
||||||
|
|
||||||
expect(router._activeView).eql(view);
|
expect(router._activeView).eql({
|
||||||
|
type: "backbone",
|
||||||
|
view: view
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load and render the passed view", function() {
|
it("should load and render the passed view", function() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ do_not_disturb=Do not disturb
|
|||||||
|
|
||||||
get_a_call_url=Get a call url
|
get_a_call_url=Get a call url
|
||||||
new_url=New url
|
new_url=New url
|
||||||
caller.placeholder=Identify this call
|
call_identifier_textinput_placeholder=Identify this call
|
||||||
|
|
||||||
unable_retrieve_url=Sorry, we were unable to retrieve a call url.
|
unable_retrieve_url=Sorry, we were unable to retrieve a call url.
|
||||||
|
|
||||||
@@ -32,6 +32,6 @@ network_disconnected=The network connection terminated abruptly.
|
|||||||
connection_error_see_console_notification=Call failed; see console for details.
|
connection_error_see_console_notification=Call failed; see console for details.
|
||||||
## LOCALIZATION NOTE (legal_text_and_links): In this item, don't translate the
|
## LOCALIZATION NOTE (legal_text_and_links): In this item, don't translate the
|
||||||
## part between {{..}}
|
## part between {{..}}
|
||||||
legal_text_and_links.innerHTML=By using this product you agree to the <a \
|
legal_text_and_links=By using this product you agree to the <a \
|
||||||
href="{{terms_of_use_url}}">Terms of Use</a> and <a \
|
target="_blank" href="{{terms_of_use_url}}">Terms of Use</a> and <a \
|
||||||
href="{{privacy_notice_url}}">Privacy Notice</a>
|
href="{{privacy_notice_url}}">Privacy Notice</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user