diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index d5f4f7b32b50..459e05360b59 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -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.privacy_url", "https://www.mozilla.org/privacy/"); 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.limit", 300000); 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.sdk", false); #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 -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 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"); diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index 8051a0ab1d8d..5395863c945a 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -13,6 +13,7 @@ Cu.import("resource:///modules/loop/LoopCalls.jsm"); Cu.import("resource:///modules/loop/MozLoopService.jsm"); Cu.import("resource:///modules/loop/LoopRooms.jsm"); Cu.import("resource:///modules/loop/LoopContacts.jsm"); +Cu.importGlobalProperties(["Blob"]); XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts", "resource:///modules/loop/LoopContacts.jsm"); @@ -685,6 +686,31 @@ function injectLoopAPI(targetWindow) { 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) { diff --git a/browser/components/loop/content/js/conversation.js b/browser/components/loop/content/js/conversation.js index 9b5192c55fcc..9bd7534f5823 100644 --- a/browser/components/loop/content/js/conversation.js +++ b/browser/components/loop/content/js/conversation.js @@ -21,7 +21,7 @@ loop.conversation = (function(mozL10n) { var DesktopRoomView = loop.roomViews.DesktopRoomView; var IncomingCallView = React.createClass({displayName: 'IncomingCallView', - mixins: [sharedMixins.DropdownMenuMixin], + mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin], propTypes: { model: React.PropTypes.object.isRequired, @@ -185,10 +185,16 @@ loop.conversation = (function(mozL10n) { * incoming call views (bug 1088672). */ var GenericFailureView = React.createClass({displayName: 'GenericFailureView', + mixins: [sharedMixins.AudioMixin], + propTypes: { cancelCall: React.PropTypes.func.isRequired }, + componentDidMount: function() { + this.play("failure"); + }, + render: function() { document.title = mozL10n.get("generic_failure_title"); diff --git a/browser/components/loop/content/js/conversation.jsx b/browser/components/loop/content/js/conversation.jsx index 8ed6b960be7e..dcac1b60f263 100644 --- a/browser/components/loop/content/js/conversation.jsx +++ b/browser/components/loop/content/js/conversation.jsx @@ -21,7 +21,7 @@ loop.conversation = (function(mozL10n) { var DesktopRoomView = loop.roomViews.DesktopRoomView; var IncomingCallView = React.createClass({ - mixins: [sharedMixins.DropdownMenuMixin], + mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin], propTypes: { model: React.PropTypes.object.isRequired, @@ -185,10 +185,16 @@ loop.conversation = (function(mozL10n) { * incoming call views (bug 1088672). */ var GenericFailureView = React.createClass({ + mixins: [sharedMixins.AudioMixin], + propTypes: { cancelCall: React.PropTypes.func.isRequired }, + componentDidMount: function() { + this.play("failure"); + }, + render: function() { document.title = mozL10n.get("generic_failure_title"); diff --git a/browser/components/loop/content/js/conversationViews.js b/browser/components/loop/content/js/conversationViews.js index d1423b55e876..b9f2505da6d6 100644 --- a/browser/components/loop/content/js/conversationViews.js +++ b/browser/components/loop/content/js/conversationViews.js @@ -14,6 +14,7 @@ loop.conversationViews = (function(mozL10n) { var sharedActions = loop.shared.actions; var sharedUtils = loop.shared.utils; var sharedViews = loop.shared.views; + var sharedMixins = loop.shared.mixins; // 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 @@ -133,6 +134,8 @@ loop.conversationViews = (function(mozL10n) { * pending/ringing strings. */ var PendingConversationView = React.createClass({displayName: 'PendingConversationView', + mixins: [sharedMixins.AudioMixin], + propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, callState: React.PropTypes.string, @@ -146,6 +149,10 @@ loop.conversationViews = (function(mozL10n) { }; }, + componentDidMount: function() { + this.play("ringtone", {loop: true}); + }, + cancelCall: function() { this.props.dispatcher.dispatch(new sharedActions.CancelCall()); }, @@ -186,7 +193,7 @@ loop.conversationViews = (function(mozL10n) { * Call failed view. Displayed when a call fails. */ var CallFailedView = React.createClass({displayName: 'CallFailedView', - mixins: [Backbone.Events], + mixins: [Backbone.Events, sharedMixins.AudioMixin], propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, @@ -205,6 +212,7 @@ loop.conversationViews = (function(mozL10n) { }, componentDidMount: function() { + this.play("failure"); this.listenTo(this.props.store, "change:emailLink", this._onEmailLinkReceived); this.listenTo(this.props.store, "error:emailLink", diff --git a/browser/components/loop/content/js/conversationViews.jsx b/browser/components/loop/content/js/conversationViews.jsx index 1ad0707fc881..70fdc54f8911 100644 --- a/browser/components/loop/content/js/conversationViews.jsx +++ b/browser/components/loop/content/js/conversationViews.jsx @@ -14,6 +14,7 @@ loop.conversationViews = (function(mozL10n) { var sharedActions = loop.shared.actions; var sharedUtils = loop.shared.utils; var sharedViews = loop.shared.views; + var sharedMixins = loop.shared.mixins; // 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 @@ -133,6 +134,8 @@ loop.conversationViews = (function(mozL10n) { * pending/ringing strings. */ var PendingConversationView = React.createClass({ + mixins: [sharedMixins.AudioMixin], + propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, callState: React.PropTypes.string, @@ -146,6 +149,10 @@ loop.conversationViews = (function(mozL10n) { }; }, + componentDidMount: function() { + this.play("ringtone", {loop: true}); + }, + cancelCall: function() { this.props.dispatcher.dispatch(new sharedActions.CancelCall()); }, @@ -186,7 +193,7 @@ loop.conversationViews = (function(mozL10n) { * Call failed view. Displayed when a call fails. */ var CallFailedView = React.createClass({ - mixins: [Backbone.Events], + mixins: [Backbone.Events, sharedMixins.AudioMixin], propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, @@ -205,6 +212,7 @@ loop.conversationViews = (function(mozL10n) { }, componentDidMount: function() { + this.play("failure"); this.listenTo(this.props.store, "change:emailLink", this._onEmailLinkReceived); this.listenTo(this.props.store, "error:emailLink", diff --git a/browser/components/loop/content/shared/js/mixins.js b/browser/components/loop/content/shared/js/mixins.js index 222663f3370d..9242d33bbe99 100644 --- a/browser/components/loop/content/shared/js/mixins.js +++ b/browser/components/loop/content/shared/js/mixins.js @@ -141,6 +141,7 @@ loop.shared.mixins = (function() { */ var AudioMixin = { audio: null, + _audioRequest: null, _isLoopDesktop: function() { 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. * - * @param {String} filename The filename to play (excluding the extension). + * @param {String} name The filename to play (excluding the extension). */ - play: function(filename, options) { - if (this._isLoopDesktop()) { - // XXX: We need navigator.mozLoop.playSound(name), see Bug 1089585. - return; - } - + play: function(name, options) { options = options || {}; options.loop = options.loop || false; this._ensureAudioStopped(); - this.audio = new Audio('shared/sounds/' + filename + ".ogg"); - this.audio.loop = options.loop; - this.audio.play(); + this._getAudioBlob(name, function(error, blob) { + if (error) { + 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. */ _ensureAudioStopped: function() { + if (this._audioRequest) { + this._audioRequest.abort(); + delete this._audioRequest; + } + if (this.audio) { this.audio.pause(); this.audio.removeAttribute("src"); diff --git a/browser/components/loop/content/shared/js/views.js b/browser/components/loop/content/shared/js/views.js index 94322bbda7e4..66b33d85f576 100644 --- a/browser/components/loop/content/shared/js/views.js +++ b/browser/components/loop/content/shared/js/views.js @@ -540,6 +540,8 @@ loop.shared.views = (function(_, OT, l10n) { * Feedback view. */ var FeedbackView = React.createClass({displayName: 'FeedbackView', + mixins: [sharedMixins.AudioMixin], + propTypes: { // A loop.FeedbackAPIClient instance feedbackApiClient: React.PropTypes.object.isRequired, @@ -556,6 +558,10 @@ loop.shared.views = (function(_, OT, l10n) { return {step: "start"}; }, + componentDidMount: function() { + this.play("terminated"); + }, + reset: function() { this.setState(this.getInitialState()); }, diff --git a/browser/components/loop/content/shared/js/views.jsx b/browser/components/loop/content/shared/js/views.jsx index 3c89b1bcb7f9..d49299e6f582 100644 --- a/browser/components/loop/content/shared/js/views.jsx +++ b/browser/components/loop/content/shared/js/views.jsx @@ -540,6 +540,8 @@ loop.shared.views = (function(_, OT, l10n) { * Feedback view. */ var FeedbackView = React.createClass({ + mixins: [sharedMixins.AudioMixin], + propTypes: { // A loop.FeedbackAPIClient instance feedbackApiClient: React.PropTypes.object.isRequired, @@ -556,6 +558,10 @@ loop.shared.views = (function(_, OT, l10n) { return {step: "start"}; }, + componentDidMount: function() { + this.play("terminated"); + }, + reset: function() { this.setState(this.getInitialState()); }, diff --git a/browser/components/loop/content/shared/sounds/Firefox-Long.ogg b/browser/components/loop/content/shared/sounds/Firefox-Long.ogg deleted file mode 100644 index e78550d18a82..000000000000 Binary files a/browser/components/loop/content/shared/sounds/Firefox-Long.ogg and /dev/null differ diff --git a/browser/components/loop/content/shared/sounds/ringing.ogg b/browser/components/loop/content/shared/sounds/ringtone.ogg similarity index 98% rename from browser/components/loop/content/shared/sounds/ringing.ogg rename to browser/components/loop/content/shared/sounds/ringtone.ogg index d3bb331da001..a4ddffbb336a 100644 Binary files a/browser/components/loop/content/shared/sounds/ringing.ogg and b/browser/components/loop/content/shared/sounds/ringtone.ogg differ diff --git a/browser/components/loop/jar.mn b/browser/components/loop/jar.mn index 6e6c46497019..a3efabb9128e 100644 --- a/browser/components/loop/jar.mn +++ b/browser/components/loop/jar.mn @@ -81,7 +81,11 @@ browser.jar: content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js) # 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 content/browser/loop/libs/sdk.js (content/shared/libs/sdk.js) diff --git a/browser/components/loop/standalone/content/js/webapp.js b/browser/components/loop/standalone/content/js/webapp.js index 6aa141830090..976d423465aa 100644 --- a/browser/components/loop/standalone/content/js/webapp.js +++ b/browser/components/loop/standalone/content/js/webapp.js @@ -286,7 +286,7 @@ loop.webapp = (function($, _, OT, mozL10n) { }, _handleRingingProgress: function() { - this.play("ringing", {loop: true}); + this.play("ringtone", {loop: true}); this.setState({callState: "ringing"}); }, @@ -534,8 +534,6 @@ loop.webapp = (function($, _, OT, mozL10n) { * Ended conversation view. */ var EndedConversationView = React.createClass({displayName: 'EndedConversationView', - mixins: [sharedMixins.AudioMixin], - propTypes: { conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) .isRequired, @@ -544,10 +542,6 @@ loop.webapp = (function($, _, OT, mozL10n) { onAfterFeedbackReceived: React.PropTypes.func.isRequired }, - componentDidMount: function() { - this.play("terminated"); - }, - render: function() { document.title = mozL10n.get("standalone_title_with_status", {clientShortname: mozL10n.get("clientShortname2"), diff --git a/browser/components/loop/standalone/content/js/webapp.jsx b/browser/components/loop/standalone/content/js/webapp.jsx index 2a868a5c1139..644495f2ec28 100644 --- a/browser/components/loop/standalone/content/js/webapp.jsx +++ b/browser/components/loop/standalone/content/js/webapp.jsx @@ -286,7 +286,7 @@ loop.webapp = (function($, _, OT, mozL10n) { }, _handleRingingProgress: function() { - this.play("ringing", {loop: true}); + this.play("ringtone", {loop: true}); this.setState({callState: "ringing"}); }, @@ -534,8 +534,6 @@ loop.webapp = (function($, _, OT, mozL10n) { * Ended conversation view. */ var EndedConversationView = React.createClass({ - mixins: [sharedMixins.AudioMixin], - propTypes: { conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel) .isRequired, @@ -544,10 +542,6 @@ loop.webapp = (function($, _, OT, mozL10n) { onAfterFeedbackReceived: React.PropTypes.func.isRequired }, - componentDidMount: function() { - this.play("terminated"); - }, - render: function() { document.title = mozL10n.get("standalone_title_with_status", {clientShortname: mozL10n.get("clientShortname2"), diff --git a/browser/components/loop/test/desktop-local/conversationViews_test.js b/browser/components/loop/test/desktop-local/conversationViews_test.js index 0df21e1ff663..177cacc32cde 100644 --- a/browser/components/loop/test/desktop-local/conversationViews_test.js +++ b/browser/components/loop/test/desktop-local/conversationViews_test.js @@ -7,7 +7,7 @@ describe("loop.conversationViews", function () { "use strict"; 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; @@ -30,11 +30,39 @@ describe("loop.conversationViews", function () { 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() { document.title = oldTitle; view = undefined; + delete navigator.mozLoop; sandbox.restore(); }); @@ -202,7 +230,7 @@ describe("loop.conversationViews", function () { }); describe("CallFailedView", function() { - var store; + var store, fakeAudio; function mountTestComponent(props) { return TestUtils.renderIntoDocument( @@ -219,6 +247,12 @@ describe("loop.conversationViews", function () { client: {}, 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", @@ -306,6 +340,16 @@ describe("loop.conversationViews", function () { 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() { @@ -412,11 +456,6 @@ describe("loop.conversationViews", function () { } beforeEach(function() { - navigator.mozLoop = { - getLoopCharPref: function() { return "fake"; }, - appVersionInfo: sinon.spy() - }; - store = new loop.store.ConversationStore({}, { dispatcher: dispatcher, 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'", function() { store.set({callState: CALL_STATES.TERMINATED}); diff --git a/browser/components/loop/test/desktop-local/conversation_test.js b/browser/components/loop/test/desktop-local/conversation_test.js index 34f755214de2..1eb494042562 100644 --- a/browser/components/loop/test/desktop-local/conversation_test.js +++ b/browser/components/loop/test/desktop-local/conversation_test.js @@ -57,7 +57,10 @@ describe("loop.conversation", function() { channel: "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 @@ -690,8 +693,8 @@ describe("loop.conversation", function() { function() { conversation.trigger("session:network-disconnected"); - TestUtils.findRenderedComponentWithType(icView, - loop.conversation.GenericFailureView); + TestUtils.findRenderedComponentWithType(icView, + loop.conversation.GenericFailureView); }); it("should update the conversation window toolbar title", @@ -747,7 +750,7 @@ describe("loop.conversation", function() { }); describe("IncomingCallView", function() { - var view, model; + var view, model, fakeAudio; beforeEach(function() { var Model = Backbone.Model.extend({ @@ -757,6 +760,13 @@ describe("loop.conversation", function() { sandbox.spy(model, "trigger"); 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({ model: model, 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); + }); + + }); }); diff --git a/browser/components/loop/test/shared/views_test.js b/browser/components/loop/test/shared/views_test.js index 522076498649..0b8d307cf082 100644 --- a/browser/components/loop/test/shared/views_test.js +++ b/browser/components/loop/test/shared/views_test.js @@ -15,7 +15,7 @@ describe("loop.shared.views", function() { var sharedModels = loop.shared.models, sharedViews = loop.shared.views, getReactElementByClass = TestUtils.findRenderedDOMComponentWithClass, - sandbox; + sandbox, fakeAudioXHR; beforeEach(function() { sandbox = sinon.sandbox.create(); @@ -23,6 +23,18 @@ describe("loop.shared.views", function() { sandbox.stub(l10n, "get", function(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() { @@ -368,16 +380,55 @@ describe("loop.shared.views", function() { it("should play a connected sound, once, on session:connected", function() { + var url = "shared/sounds/connected.ogg"; + sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR); model.trigger("session:connected"); - sinon.assert.calledOnce(window.Audio); - sinon.assert.calledWithExactly( - window.Audio, "shared/sounds/connected.ogg"); + fakeAudioXHR.onload(); + + 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); }); }); describe("for both (standalone and desktop)", function() { + beforeEach(function() { + sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR); + }); + it("should start streaming on session:connected", function() { model.trigger("session:connected"); @@ -458,6 +509,7 @@ describe("loop.shared.views", function() { beforeEach(function() { fakeFeedbackApiClient = {send: sandbox.stub()}; + sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR); comp = TestUtils.renderIntoDocument(sharedViews.FeedbackView({ feedbackApiClient: fakeFeedbackApiClient })); diff --git a/browser/components/loop/test/standalone/webapp_test.js b/browser/components/loop/test/standalone/webapp_test.js index 930f560b01ab..017a03c12671 100644 --- a/browser/components/loop/test/standalone/webapp_test.js +++ b/browser/components/loop/test/standalone/webapp_test.js @@ -18,7 +18,8 @@ describe("loop.webapp", function() { sandbox, notifications, feedbackApiClient, - stubGetPermsAndCacheMedia; + stubGetPermsAndCacheMedia, + fakeAudioXHR; beforeEach(function() { sandbox = sinon.sandbox.create(); @@ -29,6 +30,19 @@ describe("loop.webapp", function() { stubGetPermsAndCacheMedia = sandbox.stub( 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() { @@ -219,6 +233,7 @@ describe("loop.webapp", function() { describe("state: terminate, reason: reject", function() { beforeEach(function() { sandbox.stub(notifications, "errorL10n"); + sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR); }); it("should display the FailedConversationView", function() { @@ -307,6 +322,7 @@ describe("loop.webapp", function() { promiseConnectStub = sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect"); promiseConnectStub.returns(new Promise(function(resolve, reject) {})); + sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR); }); describe("call:outgoing", function() { @@ -526,6 +542,8 @@ describe("loop.webapp", function() { var view, conversation, client, fakeAudio; beforeEach(function() { + sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR); + fakeAudio = { play: sinon.spy(), pause: sinon.spy(), @@ -541,6 +559,7 @@ describe("loop.webapp", function() { }); conversation.set("loopToken", "fakeToken"); + sandbox.stub(client, "requestCallUrlInfo"); view = React.addons.TestUtils.renderIntoDocument( loop.webapp.FailedConversationView({ conversation: conversation, @@ -550,9 +569,12 @@ describe("loop.webapp", function() { }); it("should play a failure sound, once", function() { - sinon.assert.calledOnce(window.Audio); - sinon.assert.calledWithExactly(window.Audio, - "shared/sounds/failure.ogg"); + fakeAudioXHR.onload(); + + 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); }); }); @@ -678,6 +700,7 @@ describe("loop.webapp", function() { removeAttribute: sinon.spy() }; sandbox.stub(window, "Audio").returns(fakeAudio); + sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR); view = React.addons.TestUtils.renderIntoDocument( loop.webapp.PendingConversationView({ @@ -689,8 +712,12 @@ describe("loop.webapp", function() { describe("#componentDidMount", function() { it("should play a looped connecting sound", function() { - sinon.assert.calledOnce(window.Audio); - sinon.assert.calledWithExactly(window.Audio, "shared/sounds/connecting.ogg"); + fakeAudioXHR.onload(); + + 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); }); @@ -727,8 +754,13 @@ describe("loop.webapp", function() { it("should play a looped ringing sound", function() { 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); }); }); @@ -997,6 +1029,7 @@ describe("loop.webapp", function() { conversation = new sharedModels.ConversationModel({}, { sdk: {} }); + sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR); view = React.addons.TestUtils.renderIntoDocument( loop.webapp.EndedConversationView({ conversation: conversation, @@ -1018,8 +1051,13 @@ describe("loop.webapp", function() { describe("#componentDidMount", function() { it("should play a terminating sound, once", function() { - sinon.assert.calledOnce(window.Audio); - sinon.assert.calledWithExactly(window.Audio, "shared/sounds/terminated.ogg"); + fakeAudioXHR.onload(); + + 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); });