Bug 1065201: introduce new sounds for Hello standalone and desktop. r=mikedeboer

This commit is contained in:
Romain Gauthier 2014-11-06 14:51:50 +01:00
Родитель 61eeb953f5
Коммит 002e180974
18 изменённых файлов: 317 добавлений и 60 удалений

Просмотреть файл

@ -1650,7 +1650,7 @@ pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
pref("loop.legal.ToS_url", "https://hello.firefox.com/legal/terms/"); pref("loop.legal.ToS_url", "https://hello.firefox.com/legal/terms/");
pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/"); pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/");
pref("loop.do_not_disturb", false); pref("loop.do_not_disturb", false);
pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/Firefox-Long.ogg"); pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/ringtone.ogg");
pref("loop.retry_delay.start", 60000); pref("loop.retry_delay.start", 60000);
pref("loop.retry_delay.limit", 300000); pref("loop.retry_delay.limit", 300000);
pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback"); pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
@ -1660,9 +1660,9 @@ pref("loop.debug.dispatcher", false);
pref("loop.debug.websocket", false); pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false); pref("loop.debug.sdk", false);
#ifdef DEBUG #ifdef DEBUG
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*"); pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
#else #else
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net"); pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
#endif #endif
pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto"); pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds"); pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");

Просмотреть файл

@ -13,6 +13,7 @@ Cu.import("resource:///modules/loop/LoopCalls.jsm");
Cu.import("resource:///modules/loop/MozLoopService.jsm"); Cu.import("resource:///modules/loop/MozLoopService.jsm");
Cu.import("resource:///modules/loop/LoopRooms.jsm"); Cu.import("resource:///modules/loop/LoopRooms.jsm");
Cu.import("resource:///modules/loop/LoopContacts.jsm"); Cu.import("resource:///modules/loop/LoopContacts.jsm");
Cu.importGlobalProperties(["Blob"]);
XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts", XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
"resource:///modules/loop/LoopContacts.jsm"); "resource:///modules/loop/LoopContacts.jsm");
@ -685,6 +686,31 @@ function injectLoopAPI(targetWindow) {
return MozLoopService.generateUUID(); return MozLoopService.generateUUID();
} }
}, },
getAudioBlob: {
enumerable: true,
writable: true,
value: function(name, callback) {
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
let url = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
request.open("GET", url, true);
request.responseType = "arraybuffer";
request.onload = () => {
if (request.status < 200 || request.status >= 300) {
let error = new Error(request.status + " " + request.statusText);
callback(cloneValueInto(error, targetWindow));
return;
}
let blob = new Blob([request.response], {type: "audio/ogg"});
callback(null, cloneValueInto(blob, targetWindow));
};
request.send();
}
}
}; };
function onStatusChanged(aSubject, aTopic, aData) { function onStatusChanged(aSubject, aTopic, aData) {

Просмотреть файл

@ -21,7 +21,7 @@ loop.conversation = (function(mozL10n) {
var DesktopRoomView = loop.roomViews.DesktopRoomView; var DesktopRoomView = loop.roomViews.DesktopRoomView;
var IncomingCallView = React.createClass({displayName: 'IncomingCallView', var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
mixins: [sharedMixins.DropdownMenuMixin], mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
propTypes: { propTypes: {
model: React.PropTypes.object.isRequired, model: React.PropTypes.object.isRequired,
@ -185,10 +185,16 @@ loop.conversation = (function(mozL10n) {
* incoming call views (bug 1088672). * incoming call views (bug 1088672).
*/ */
var GenericFailureView = React.createClass({displayName: 'GenericFailureView', var GenericFailureView = React.createClass({displayName: 'GenericFailureView',
mixins: [sharedMixins.AudioMixin],
propTypes: { propTypes: {
cancelCall: React.PropTypes.func.isRequired cancelCall: React.PropTypes.func.isRequired
}, },
componentDidMount: function() {
this.play("failure");
},
render: function() { render: function() {
document.title = mozL10n.get("generic_failure_title"); document.title = mozL10n.get("generic_failure_title");

Просмотреть файл

@ -21,7 +21,7 @@ loop.conversation = (function(mozL10n) {
var DesktopRoomView = loop.roomViews.DesktopRoomView; var DesktopRoomView = loop.roomViews.DesktopRoomView;
var IncomingCallView = React.createClass({ var IncomingCallView = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin], mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
propTypes: { propTypes: {
model: React.PropTypes.object.isRequired, model: React.PropTypes.object.isRequired,
@ -185,10 +185,16 @@ loop.conversation = (function(mozL10n) {
* incoming call views (bug 1088672). * incoming call views (bug 1088672).
*/ */
var GenericFailureView = React.createClass({ var GenericFailureView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: { propTypes: {
cancelCall: React.PropTypes.func.isRequired cancelCall: React.PropTypes.func.isRequired
}, },
componentDidMount: function() {
this.play("failure");
},
render: function() { render: function() {
document.title = mozL10n.get("generic_failure_title"); document.title = mozL10n.get("generic_failure_title");

Просмотреть файл

@ -14,6 +14,7 @@ loop.conversationViews = (function(mozL10n) {
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils; var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views; var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
// This duplicates a similar function in contacts.jsx that isn't used in the // This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider // conversation window. If we get too many of these, we might want to consider
@ -133,6 +134,8 @@ loop.conversationViews = (function(mozL10n) {
* pending/ringing strings. * pending/ringing strings.
*/ */
var PendingConversationView = React.createClass({displayName: 'PendingConversationView', var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
mixins: [sharedMixins.AudioMixin],
propTypes: { propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
callState: React.PropTypes.string, callState: React.PropTypes.string,
@ -146,6 +149,10 @@ loop.conversationViews = (function(mozL10n) {
}; };
}, },
componentDidMount: function() {
this.play("ringtone", {loop: true});
},
cancelCall: function() { cancelCall: function() {
this.props.dispatcher.dispatch(new sharedActions.CancelCall()); this.props.dispatcher.dispatch(new sharedActions.CancelCall());
}, },
@ -186,7 +193,7 @@ loop.conversationViews = (function(mozL10n) {
* Call failed view. Displayed when a call fails. * Call failed view. Displayed when a call fails.
*/ */
var CallFailedView = React.createClass({displayName: 'CallFailedView', var CallFailedView = React.createClass({displayName: 'CallFailedView',
mixins: [Backbone.Events], mixins: [Backbone.Events, sharedMixins.AudioMixin],
propTypes: { propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@ -205,6 +212,7 @@ loop.conversationViews = (function(mozL10n) {
}, },
componentDidMount: function() { componentDidMount: function() {
this.play("failure");
this.listenTo(this.props.store, "change:emailLink", this.listenTo(this.props.store, "change:emailLink",
this._onEmailLinkReceived); this._onEmailLinkReceived);
this.listenTo(this.props.store, "error:emailLink", this.listenTo(this.props.store, "error:emailLink",

Просмотреть файл

@ -14,6 +14,7 @@ loop.conversationViews = (function(mozL10n) {
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils; var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views; var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
// This duplicates a similar function in contacts.jsx that isn't used in the // This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider // conversation window. If we get too many of these, we might want to consider
@ -133,6 +134,8 @@ loop.conversationViews = (function(mozL10n) {
* pending/ringing strings. * pending/ringing strings.
*/ */
var PendingConversationView = React.createClass({ var PendingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: { propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
callState: React.PropTypes.string, callState: React.PropTypes.string,
@ -146,6 +149,10 @@ loop.conversationViews = (function(mozL10n) {
}; };
}, },
componentDidMount: function() {
this.play("ringtone", {loop: true});
},
cancelCall: function() { cancelCall: function() {
this.props.dispatcher.dispatch(new sharedActions.CancelCall()); this.props.dispatcher.dispatch(new sharedActions.CancelCall());
}, },
@ -186,7 +193,7 @@ loop.conversationViews = (function(mozL10n) {
* Call failed view. Displayed when a call fails. * Call failed view. Displayed when a call fails.
*/ */
var CallFailedView = React.createClass({ var CallFailedView = React.createClass({
mixins: [Backbone.Events], mixins: [Backbone.Events, sharedMixins.AudioMixin],
propTypes: { propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@ -205,6 +212,7 @@ loop.conversationViews = (function(mozL10n) {
}, },
componentDidMount: function() { componentDidMount: function() {
this.play("failure");
this.listenTo(this.props.store, "change:emailLink", this.listenTo(this.props.store, "change:emailLink",
this._onEmailLinkReceived); this._onEmailLinkReceived);
this.listenTo(this.props.store, "error:emailLink", this.listenTo(this.props.store, "error:emailLink",

Просмотреть файл

@ -141,6 +141,7 @@ loop.shared.mixins = (function() {
*/ */
var AudioMixin = { var AudioMixin = {
audio: null, audio: null,
_audioRequest: null,
_isLoopDesktop: function() { _isLoopDesktop: function() {
return typeof rootObject.navigator.mozLoop === "object"; return typeof rootObject.navigator.mozLoop === "object";
@ -149,27 +150,62 @@ loop.shared.mixins = (function() {
/** /**
* Starts playing an audio file, stopping any audio that is already in progress. * Starts playing an audio file, stopping any audio that is already in progress.
* *
* @param {String} filename The filename to play (excluding the extension). * @param {String} name The filename to play (excluding the extension).
*/ */
play: function(filename, options) { play: function(name, options) {
if (this._isLoopDesktop()) {
// XXX: We need navigator.mozLoop.playSound(name), see Bug 1089585.
return;
}
options = options || {}; options = options || {};
options.loop = options.loop || false; options.loop = options.loop || false;
this._ensureAudioStopped(); this._ensureAudioStopped();
this.audio = new Audio('shared/sounds/' + filename + ".ogg"); this._getAudioBlob(name, function(error, blob) {
this.audio.loop = options.loop; if (error) {
this.audio.play(); console.error(error);
return;
}
var url = URL.createObjectURL(blob);
this.audio = new Audio(url);
this.audio.loop = options.loop;
this.audio.play();
}.bind(this));
},
_getAudioBlob: function(name, callback) {
if (this._isLoopDesktop()) {
rootObject.navigator.mozLoop.getAudioBlob(name, callback);
return;
}
var url = "shared/sounds/" + name + ".ogg";
this._audioRequest = new XMLHttpRequest();
this._audioRequest.open("GET", url, true);
this._audioRequest.responseType = "arraybuffer";
this._audioRequest.onload = function() {
var request = this._audioRequest;
var error;
if (request.status < 200 || request.status >= 300) {
error = new Error(request.status + " " + request.statusText);
callback(error);
return;
}
var type = request.getResponseHeader("Content-Type");
var blob = new Blob([request.response], {type: type});
callback(null, blob);
}.bind(this);
this._audioRequest.send(null);
}, },
/** /**
* Ensures audio is stopped playing, and removes the object from memory. * Ensures audio is stopped playing, and removes the object from memory.
*/ */
_ensureAudioStopped: function() { _ensureAudioStopped: function() {
if (this._audioRequest) {
this._audioRequest.abort();
delete this._audioRequest;
}
if (this.audio) { if (this.audio) {
this.audio.pause(); this.audio.pause();
this.audio.removeAttribute("src"); this.audio.removeAttribute("src");

Просмотреть файл

@ -540,6 +540,8 @@ loop.shared.views = (function(_, OT, l10n) {
* Feedback view. * Feedback view.
*/ */
var FeedbackView = React.createClass({displayName: 'FeedbackView', var FeedbackView = React.createClass({displayName: 'FeedbackView',
mixins: [sharedMixins.AudioMixin],
propTypes: { propTypes: {
// A loop.FeedbackAPIClient instance // A loop.FeedbackAPIClient instance
feedbackApiClient: React.PropTypes.object.isRequired, feedbackApiClient: React.PropTypes.object.isRequired,
@ -556,6 +558,10 @@ loop.shared.views = (function(_, OT, l10n) {
return {step: "start"}; return {step: "start"};
}, },
componentDidMount: function() {
this.play("terminated");
},
reset: function() { reset: function() {
this.setState(this.getInitialState()); this.setState(this.getInitialState());
}, },

Просмотреть файл

@ -540,6 +540,8 @@ loop.shared.views = (function(_, OT, l10n) {
* Feedback view. * Feedback view.
*/ */
var FeedbackView = React.createClass({ var FeedbackView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: { propTypes: {
// A loop.FeedbackAPIClient instance // A loop.FeedbackAPIClient instance
feedbackApiClient: React.PropTypes.object.isRequired, feedbackApiClient: React.PropTypes.object.isRequired,
@ -556,6 +558,10 @@ loop.shared.views = (function(_, OT, l10n) {
return {step: "start"}; return {step: "start"};
}, },
componentDidMount: function() {
this.play("terminated");
},
reset: function() { reset: function() {
this.setState(this.getInitialState()); this.setState(this.getInitialState());
}, },

Двоичный файл не отображается.

Просмотреть файл

@ -81,7 +81,11 @@ browser.jar:
content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js) content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js)
# Shared sounds # Shared sounds
content/browser/loop/shared/sounds/Firefox-Long.ogg (content/shared/sounds/Firefox-Long.ogg) content/browser/loop/shared/sounds/ringtone.ogg (content/shared/sounds/ringtone.ogg)
content/browser/loop/shared/sounds/connecting.ogg (content/shared/sounds/connecting.ogg)
content/browser/loop/shared/sounds/connected.ogg (content/shared/sounds/connected.ogg)
content/browser/loop/shared/sounds/terminated.ogg (content/shared/sounds/terminated.ogg)
content/browser/loop/shared/sounds/failure.ogg (content/shared/sounds/failure.ogg)
# Partner SDK assets # Partner SDK assets
content/browser/loop/libs/sdk.js (content/shared/libs/sdk.js) content/browser/loop/libs/sdk.js (content/shared/libs/sdk.js)

Просмотреть файл

@ -286,7 +286,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
}, },
_handleRingingProgress: function() { _handleRingingProgress: function() {
this.play("ringing", {loop: true}); this.play("ringtone", {loop: true});
this.setState({callState: "ringing"}); this.setState({callState: "ringing"});
}, },
@ -534,8 +534,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Ended conversation view. * Ended conversation view.
*/ */
var EndedConversationView = React.createClass({displayName: 'EndedConversationView', var EndedConversationView = React.createClass({displayName: 'EndedConversationView',
mixins: [sharedMixins.AudioMixin],
propTypes: { propTypes: {
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired, .isRequired,
@ -544,10 +542,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
onAfterFeedbackReceived: React.PropTypes.func.isRequired onAfterFeedbackReceived: React.PropTypes.func.isRequired
}, },
componentDidMount: function() {
this.play("terminated");
},
render: function() { render: function() {
document.title = mozL10n.get("standalone_title_with_status", document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"), {clientShortname: mozL10n.get("clientShortname2"),

Просмотреть файл

@ -286,7 +286,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
}, },
_handleRingingProgress: function() { _handleRingingProgress: function() {
this.play("ringing", {loop: true}); this.play("ringtone", {loop: true});
this.setState({callState: "ringing"}); this.setState({callState: "ringing"});
}, },
@ -534,8 +534,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Ended conversation view. * Ended conversation view.
*/ */
var EndedConversationView = React.createClass({ var EndedConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: { propTypes: {
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired, .isRequired,
@ -544,10 +542,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
onAfterFeedbackReceived: React.PropTypes.func.isRequired onAfterFeedbackReceived: React.PropTypes.func.isRequired
}, },
componentDidMount: function() {
this.play("terminated");
},
render: function() { render: function() {
document.title = mozL10n.get("standalone_title_with_status", document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"), {clientShortname: mozL10n.get("clientShortname2"),

Просмотреть файл

@ -7,7 +7,7 @@ describe("loop.conversationViews", function () {
"use strict"; "use strict";
var sharedUtils = loop.shared.utils; var sharedUtils = loop.shared.utils;
var sandbox, oldTitle, view, dispatcher, contact; var sandbox, oldTitle, view, dispatcher, contact, fakeAudioXHR;
var CALL_STATES = loop.store.CALL_STATES; var CALL_STATES = loop.store.CALL_STATES;
@ -30,11 +30,39 @@ describe("loop.conversationViews", function () {
pref: true pref: true
}] }]
}; };
fakeAudioXHR = {
open: sinon.spy(),
send: function() {},
abort: function() {},
getResponseHeader: function(header) {
if (header === "Content-Type")
return "audio/ogg";
},
responseType: null,
response: new ArrayBuffer(10),
onload: null
};
navigator.mozLoop = {
getLoopCharPref: sinon.stub().returns("http://fakeurl"),
composeEmail: sinon.spy(),
get appVersionInfo() {
return {
version: "42",
channel: "test",
platform: "test"
};
},
getAudioBlob: sinon.spy(function(name, callback) {
callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"}));
})
};
}); });
afterEach(function() { afterEach(function() {
document.title = oldTitle; document.title = oldTitle;
view = undefined; view = undefined;
delete navigator.mozLoop;
sandbox.restore(); sandbox.restore();
}); });
@ -202,7 +230,7 @@ describe("loop.conversationViews", function () {
}); });
describe("CallFailedView", function() { describe("CallFailedView", function() {
var store; var store, fakeAudio;
function mountTestComponent(props) { function mountTestComponent(props) {
return TestUtils.renderIntoDocument( return TestUtils.renderIntoDocument(
@ -219,6 +247,12 @@ describe("loop.conversationViews", function () {
client: {}, client: {},
sdkDriver: {} sdkDriver: {}
}); });
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
}); });
it("should dispatch a retryCall action when the retry button is pressed", it("should dispatch a retryCall action when the retry button is pressed",
@ -306,6 +340,16 @@ describe("loop.conversationViews", function () {
expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false); expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
}); });
it("should play a failure sound, once", function() {
view = mountTestComponent();
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
"failure", sinon.match.func);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.equal(false);
});
}); });
describe("OngoingConversationView", function() { describe("OngoingConversationView", function() {
@ -412,11 +456,6 @@ describe("loop.conversationViews", function () {
} }
beforeEach(function() { beforeEach(function() {
navigator.mozLoop = {
getLoopCharPref: function() { return "fake"; },
appVersionInfo: sinon.spy()
};
store = new loop.store.ConversationStore({}, { store = new loop.store.ConversationStore({}, {
dispatcher: dispatcher, dispatcher: dispatcher,
client: {}, client: {},
@ -424,10 +463,6 @@ describe("loop.conversationViews", function () {
}); });
}); });
afterEach(function() {
delete navigator.mozLoop;
});
it("should render the CallFailedView when the call state is 'terminated'", it("should render the CallFailedView when the call state is 'terminated'",
function() { function() {
store.set({callState: CALL_STATES.TERMINATED}); store.set({callState: CALL_STATES.TERMINATED});

Просмотреть файл

@ -57,7 +57,10 @@ describe("loop.conversation", function() {
channel: "test", channel: "test",
platform: "test" platform: "test"
}; };
} },
getAudioBlob: sinon.spy(function(name, callback) {
callback(null, new Blob([new ArrayBuffer(10)], {type: 'audio/ogg'}));
})
}; };
// XXX These stubs should be hoisted in a common file // XXX These stubs should be hoisted in a common file
@ -690,8 +693,8 @@ describe("loop.conversation", function() {
function() { function() {
conversation.trigger("session:network-disconnected"); conversation.trigger("session:network-disconnected");
TestUtils.findRenderedComponentWithType(icView, TestUtils.findRenderedComponentWithType(icView,
loop.conversation.GenericFailureView); loop.conversation.GenericFailureView);
}); });
it("should update the conversation window toolbar title", it("should update the conversation window toolbar title",
@ -747,7 +750,7 @@ describe("loop.conversation", function() {
}); });
describe("IncomingCallView", function() { describe("IncomingCallView", function() {
var view, model; var view, model, fakeAudio;
beforeEach(function() { beforeEach(function() {
var Model = Backbone.Model.extend({ var Model = Backbone.Model.extend({
@ -757,6 +760,13 @@ describe("loop.conversation", function() {
sandbox.spy(model, "trigger"); sandbox.spy(model, "trigger");
sandbox.stub(model, "set"); sandbox.stub(model, "set");
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({ view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model, model: model,
video: true video: true
@ -896,4 +906,32 @@ describe("loop.conversation", function() {
}); });
}); });
}); });
describe("GenericFailureView", function() {
var view, fakeAudio;
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
view = TestUtils.renderIntoDocument(
loop.conversation.GenericFailureView({
cancelCall: function() {}
})
);
});
it("should play a failure sound, once", function() {
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
"failure", sinon.match.func);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.equal(false);
});
});
}); });

Просмотреть файл

@ -15,7 +15,7 @@ describe("loop.shared.views", function() {
var sharedModels = loop.shared.models, var sharedModels = loop.shared.models,
sharedViews = loop.shared.views, sharedViews = loop.shared.views,
getReactElementByClass = TestUtils.findRenderedDOMComponentWithClass, getReactElementByClass = TestUtils.findRenderedDOMComponentWithClass,
sandbox; sandbox, fakeAudioXHR;
beforeEach(function() { beforeEach(function() {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
@ -23,6 +23,18 @@ describe("loop.shared.views", function() {
sandbox.stub(l10n, "get", function(x) { sandbox.stub(l10n, "get", function(x) {
return "translated:" + x; return "translated:" + x;
}); });
fakeAudioXHR = {
open: sinon.spy(),
send: function() {},
abort: function() {},
getResponseHeader: function(header) {
if (header === "Content-Type")
return "audio/ogg";
},
responseType: null,
response: new ArrayBuffer(10),
onload: null
};
}); });
afterEach(function() { afterEach(function() {
@ -368,16 +380,55 @@ describe("loop.shared.views", function() {
it("should play a connected sound, once, on session:connected", it("should play a connected sound, once, on session:connected",
function() { function() {
var url = "shared/sounds/connected.ogg";
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
model.trigger("session:connected"); model.trigger("session:connected");
sinon.assert.calledOnce(window.Audio); fakeAudioXHR.onload();
sinon.assert.calledWithExactly(
window.Audio, "shared/sounds/connected.ogg"); sinon.assert.called(fakeAudioXHR.open);
sinon.assert.calledWithExactly(fakeAudioXHR.open, "GET", url, true);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.not.equal(true);
});
});
describe("for desktop", function() {
var origMozLoop;
beforeEach(function() {
origMozLoop = navigator.mozLoop;
navigator.mozLoop = {
getAudioBlob: sinon.spy(function(name, callback) {
var data = new ArrayBuffer(10);
callback(null, new Blob([data], {type: "audio/ogg"}));
})
};
});
afterEach(function() {
navigator.mozLoop = origMozLoop;
});
it("should play a connected sound, once, on session:connected",
function() {
var url = "chrome://browser/content/loop/shared/sounds/connected.ogg";
model.trigger("session:connected");
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
"connected", sinon.match.func);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.not.equal(true); expect(fakeAudio.loop).to.not.equal(true);
}); });
}); });
describe("for both (standalone and desktop)", function() { describe("for both (standalone and desktop)", function() {
beforeEach(function() {
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
});
it("should start streaming on session:connected", function() { it("should start streaming on session:connected", function() {
model.trigger("session:connected"); model.trigger("session:connected");
@ -458,6 +509,7 @@ describe("loop.shared.views", function() {
beforeEach(function() { beforeEach(function() {
fakeFeedbackApiClient = {send: sandbox.stub()}; fakeFeedbackApiClient = {send: sandbox.stub()};
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
comp = TestUtils.renderIntoDocument(sharedViews.FeedbackView({ comp = TestUtils.renderIntoDocument(sharedViews.FeedbackView({
feedbackApiClient: fakeFeedbackApiClient feedbackApiClient: fakeFeedbackApiClient
})); }));

Просмотреть файл

@ -18,7 +18,8 @@ describe("loop.webapp", function() {
sandbox, sandbox,
notifications, notifications,
feedbackApiClient, feedbackApiClient,
stubGetPermsAndCacheMedia; stubGetPermsAndCacheMedia,
fakeAudioXHR;
beforeEach(function() { beforeEach(function() {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
@ -29,6 +30,19 @@ describe("loop.webapp", function() {
stubGetPermsAndCacheMedia = sandbox.stub( stubGetPermsAndCacheMedia = sandbox.stub(
loop.standaloneMedia._MultiplexGum.prototype, "getPermsAndCacheMedia"); loop.standaloneMedia._MultiplexGum.prototype, "getPermsAndCacheMedia");
fakeAudioXHR = {
open: sinon.spy(),
send: function() {},
abort: function() {},
getResponseHeader: function(header) {
if (header === "Content-Type")
return "audio/ogg";
},
responseType: null,
response: new ArrayBuffer(10),
onload: null
};
}); });
afterEach(function() { afterEach(function() {
@ -219,6 +233,7 @@ describe("loop.webapp", function() {
describe("state: terminate, reason: reject", function() { describe("state: terminate, reason: reject", function() {
beforeEach(function() { beforeEach(function() {
sandbox.stub(notifications, "errorL10n"); sandbox.stub(notifications, "errorL10n");
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
}); });
it("should display the FailedConversationView", function() { it("should display the FailedConversationView", function() {
@ -307,6 +322,7 @@ describe("loop.webapp", function() {
promiseConnectStub = promiseConnectStub =
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect"); sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect");
promiseConnectStub.returns(new Promise(function(resolve, reject) {})); promiseConnectStub.returns(new Promise(function(resolve, reject) {}));
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
}); });
describe("call:outgoing", function() { describe("call:outgoing", function() {
@ -526,6 +542,8 @@ describe("loop.webapp", function() {
var view, conversation, client, fakeAudio; var view, conversation, client, fakeAudio;
beforeEach(function() { beforeEach(function() {
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
fakeAudio = { fakeAudio = {
play: sinon.spy(), play: sinon.spy(),
pause: sinon.spy(), pause: sinon.spy(),
@ -541,6 +559,7 @@ describe("loop.webapp", function() {
}); });
conversation.set("loopToken", "fakeToken"); conversation.set("loopToken", "fakeToken");
sandbox.stub(client, "requestCallUrlInfo");
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.FailedConversationView({ loop.webapp.FailedConversationView({
conversation: conversation, conversation: conversation,
@ -550,9 +569,12 @@ describe("loop.webapp", function() {
}); });
it("should play a failure sound, once", function() { it("should play a failure sound, once", function() {
sinon.assert.calledOnce(window.Audio); fakeAudioXHR.onload();
sinon.assert.calledWithExactly(window.Audio,
"shared/sounds/failure.ogg"); sinon.assert.called(fakeAudioXHR.open);
sinon.assert.calledWithExactly(
fakeAudioXHR.open, "GET", "shared/sounds/failure.ogg", true);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.equal(false); expect(fakeAudio.loop).to.equal(false);
}); });
}); });
@ -678,6 +700,7 @@ describe("loop.webapp", function() {
removeAttribute: sinon.spy() removeAttribute: sinon.spy()
}; };
sandbox.stub(window, "Audio").returns(fakeAudio); sandbox.stub(window, "Audio").returns(fakeAudio);
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.PendingConversationView({ loop.webapp.PendingConversationView({
@ -689,8 +712,12 @@ describe("loop.webapp", function() {
describe("#componentDidMount", function() { describe("#componentDidMount", function() {
it("should play a looped connecting sound", function() { it("should play a looped connecting sound", function() {
sinon.assert.calledOnce(window.Audio); fakeAudioXHR.onload();
sinon.assert.calledWithExactly(window.Audio, "shared/sounds/connecting.ogg");
sinon.assert.called(fakeAudioXHR.open);
sinon.assert.calledWithExactly(
fakeAudioXHR.open, "GET", "shared/sounds/connecting.ogg", true);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.equal(true); expect(fakeAudio.loop).to.equal(true);
}); });
@ -727,8 +754,13 @@ describe("loop.webapp", function() {
it("should play a looped ringing sound", function() { it("should play a looped ringing sound", function() {
websocket.trigger("progress:alerting"); websocket.trigger("progress:alerting");
fakeAudioXHR.onload();
sinon.assert.calledWithExactly(window.Audio, "shared/sounds/ringing.ogg"); sinon.assert.called(fakeAudioXHR.open);
sinon.assert.calledWithExactly(
fakeAudioXHR.open, "GET", "shared/sounds/ringtone.ogg", true);
sinon.assert.called(fakeAudio.play);
expect(fakeAudio.loop).to.equal(true); expect(fakeAudio.loop).to.equal(true);
}); });
}); });
@ -997,6 +1029,7 @@ describe("loop.webapp", function() {
conversation = new sharedModels.ConversationModel({}, { conversation = new sharedModels.ConversationModel({}, {
sdk: {} sdk: {}
}); });
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.EndedConversationView({ loop.webapp.EndedConversationView({
conversation: conversation, conversation: conversation,
@ -1018,8 +1051,13 @@ describe("loop.webapp", function() {
describe("#componentDidMount", function() { describe("#componentDidMount", function() {
it("should play a terminating sound, once", function() { it("should play a terminating sound, once", function() {
sinon.assert.calledOnce(window.Audio); fakeAudioXHR.onload();
sinon.assert.calledWithExactly(window.Audio, "shared/sounds/terminated.ogg");
sinon.assert.called(fakeAudioXHR.open);
sinon.assert.calledWithExactly(
fakeAudioXHR.open, "GET", "shared/sounds/terminated.ogg", true);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.not.equal(true); expect(fakeAudio.loop).to.not.equal(true);
}); });