author | Mark Banner <standard8@mozilla.com> |
Thu, 13 Nov 2014 22:45:23 +0000 | |
changeset 215747 | bd6aae177d9c470dbdc219ee78a9c7f06b12ac1d |
parent 215746 | 100301b84f5f15a794a2f5c50b87e84e2079728d |
child 215748 | 05323e55599dde491a14c9e87dc9f7f9e415f689 |
push id | 51845 |
push user | cbook@mozilla.com |
push date | Fri, 14 Nov 2014 12:23:21 +0000 |
treeherder | mozilla-inbound@da57927b609d [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | nperriault |
bugs | 1097742 |
milestone | 36.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/components/loop/content/shared/js/activeRoomStore.js +++ b/browser/components/loop/content/shared/js/activeRoomStore.js @@ -5,16 +5,25 @@ /* global loop:true */ var loop = loop || {}; loop.store = loop.store || {}; loop.store.ActiveRoomStore = (function() { "use strict"; var sharedActions = loop.shared.actions; + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; + + // Error numbers taken from + // https://github.com/mozilla-services/loop-server/blob/master/loop/errno.json + var SERVER_CODES = loop.store.SERVER_CODES = { + INVALID_TOKEN: 105, + EXPIRED: 111, + ROOM_FULL: 202 + }; var ROOM_STATES = loop.store.ROOM_STATES = { // The initial state of the room INIT: "room-init", // The store is gathering the room data GATHER: "room-gather", // The store has got the room data READY: "room-ready", @@ -79,17 +88,18 @@ loop.store.ActiveRoomStore = (function() * @property {ROOM_STATES} roomState - the state of the room. * @property {Error=} error - if the room is an error state, this will be * set to an Error object reflecting the problem; * otherwise it will be unset. */ this._storeState = { roomState: ROOM_STATES.INIT, audioMuted: false, - videoMuted: false + videoMuted: false, + failureReason: undefined }; } ActiveRoomStore.prototype = _.extend({ /** * The time factor to adjust the expires time to ensure that we send a refresh * before the expiry. Currently set as 90%. */ @@ -107,23 +117,34 @@ loop.store.ActiveRoomStore = (function() }, /** * Handles a room failure. * * @param {sharedActions.RoomFailure} actionData */ roomFailure: function(actionData) { + function getReason(serverCode) { + switch (serverCode) { + case SERVER_CODES.INVALID_TOKEN: + case SERVER_CODES.EXPIRED: + return FAILURE_REASONS.EXPIRED_OR_INVALID; + default: + return FAILURE_REASONS.UNKNOWN; + } + } + console.error("Error in state `" + this._storeState.roomState + "`:", actionData.error); this.setStoreState({ error: actionData.error, - roomState: actionData.error.errno === 202 ? ROOM_STATES.FULL - : ROOM_STATES.FAILED + failureReason: getReason(actionData.error.errno), + roomState: actionData.error.errno === SERVER_CODES.ROOM_FULL ? + ROOM_STATES.FULL : ROOM_STATES.FAILED }); }, /** * Registers the actions with the dispatcher that this store is interested * in. */ _registerActions: function() { @@ -223,16 +244,21 @@ loop.store.ActiveRoomStore = (function() roomUrl: actionData.roomUrl }); }, /** * Handles the action to join to a room. */ joinRoom: function() { + // Reset the failure reason if necessary. + if (this.getStoreState().failureReason) { + this.setStoreState({failureReason: undefined}); + } + this._mozLoop.rooms.join(this._storeState.roomToken, function(error, responseData) { if (error) { this._dispatcher.dispatch( new sharedActions.RoomFailure({error: error})); return; } @@ -270,21 +296,27 @@ loop.store.ActiveRoomStore = (function() connectedToSdkServers: function() { this.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED }); }, /** * Handles disconnection of this local client from the sdk servers. + * + * @param {sharedActions.ConnectionFailure} actionData */ - connectionFailure: function() { + connectionFailure: function(actionData) { // Treat all reasons as something failed. In theory, clientDisconnected // could be a success case, but there's no way we should be intentionally // sending that and still have the window open. + this.setStoreState({ + failureReason: actionData.reason + }); + this._leaveRoom(ROOM_STATES.FAILED); }, /** * Records the mute state for the stream. * * @param {sharedActions.setMute} actionData The mute state for the stream type. */
--- a/browser/components/loop/content/shared/js/otSdkDriver.js +++ b/browser/components/loop/content/shared/js/otSdkDriver.js @@ -3,16 +3,17 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global loop:true */ var loop = loop || {}; loop.OTSdkDriver = (function() { var sharedActions = loop.shared.actions; + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; /** * This is a wrapper for the OT sdk. It is used to translate the SDK events into * actions, and instruct the SDK what to do as a result of actions. */ var OTSdkDriver = function(options) { if (!options.dispatcher) { throw new Error("Missing option dispatcher"); @@ -42,18 +43,21 @@ loop.OTSdkDriver = (function() { this.getLocalElement = actionData.getLocalElementFunc; this.getRemoteElement = actionData.getRemoteElementFunc; this.publisherConfig = actionData.publisherConfig; // At this state we init the publisher, even though we might be waiting for // the initial connect of the session. This saves time when setting up // the media. this.publisher = this.sdk.initPublisher(this.getLocalElement(), - this.publisherConfig, - this._onPublishComplete.bind(this)); + this.publisherConfig); + this.publisher.on("accessAllowed", this._onPublishComplete.bind(this)); + this.publisher.on("accessDenied", this._onPublishDenied.bind(this)); + this.publisher.on("accessDialogOpened", + this._onAccessDialogOpened.bind(this)); }, /** * Handles the setMute action. Informs the published stream to mute * or unmute audio as appropriate. * * @param {sharedActions.SetMute} actionData The data associated with the * action. See action.js. @@ -91,26 +95,22 @@ loop.OTSdkDriver = (function() { this._onConnectionComplete.bind(this)); }, /** * Disconnects the sdk session. */ disconnectSession: function() { if (this.session) { - this.session.off("streamCreated", this._onRemoteStreamCreated.bind(this)); - this.session.off("connectionDestroyed", - this._onConnectionDestroyed.bind(this)); - this.session.off("sessionDisconnected", - this._onSessionDisconnected.bind(this)); - + this.session.off("streamCreated connectionDestroyed sessionDisconnected"); this.session.disconnect(); delete this.session; } if (this.publisher) { + this.publisher.off("accessAllowed accessDenied accessDialogOpened"); this.publisher.destroy(); delete this.publisher; } // Also, tidy these variables ready for next time. delete this._sessionConnected; delete this._publisherReady; delete this._publishedLocalStream; @@ -121,17 +121,17 @@ loop.OTSdkDriver = (function() { * Called once the session has finished connecting. * * @param {Error} error An OT error object, null if there was no error. */ _onConnectionComplete: function(error) { if (error) { console.error("Failed to complete connection", error); this.dispatcher.dispatch(new sharedActions.ConnectionFailure({ - reason: "couldNotConnect" + reason: FAILURE_REASONS.COULD_NOT_CONNECT })); return; } this.dispatcher.dispatch(new sharedActions.ConnectedToSdkServers()); this._sessionConnected = true; this._maybePublishLocalStream(); }, @@ -154,17 +154,17 @@ loop.OTSdkDriver = (function() { * * @param {SessionDisconnectEvent} event The event details: * https://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html */ _onSessionDisconnected: function(event) { // We only need to worry about the network disconnected reason here. if (event.reason === "networkDisconnected") { this.dispatcher.dispatch(new sharedActions.ConnectionFailure({ - reason: "networkDisconnected" + reason: FAILURE_REASONS.NETWORK_DISCONNECTED })); } }, _onConnectionCreated: function(event) { if (this.session.connection.id === event.connection.id) { return; } @@ -184,34 +184,52 @@ loop.OTSdkDriver = (function() { this._subscribedRemoteStream = true; if (this._checkAllStreamsConnected()) { this.dispatcher.dispatch(new sharedActions.MediaConnected()); } }, /** + * Called from the sdk when the media access dialog is opened. + * Prevents the default action, to prevent the SDK's "allow access" + * dialog from being shown. + * + * @param {OT.Event} event + */ + _onAccessDialogOpened: function(event) { + event.preventDefault(); + }, + + /** * Handles the publishing being complete. * - * @param {Error} error An OT error object, null if there was no error. + * @param {OT.Event} event */ - _onPublishComplete: function(error) { - if (error) { - console.error("Failed to initialize publisher", error); - this.dispatcher.dispatch(new sharedActions.ConnectionFailure({ - reason: "noMedia" - })); - return; - } - + _onPublishComplete: function(event) { + event.preventDefault(); this._publisherReady = true; this._maybePublishLocalStream(); }, /** + * Handles publishing of media being denied. + * + * @param {OT.Event} event + */ + _onPublishDenied: function(event) { + // This prevents the SDK's "access denied" dialog showing. + event.preventDefault(); + + this.dispatcher.dispatch(new sharedActions.ConnectionFailure({ + reason: FAILURE_REASONS.MEDIA_DENIED + })); + }, + + /** * Publishes the local stream if the session is connected * and the publisher is ready. */ _maybePublishLocalStream: function() { if (this._sessionConnected && this._publisherReady) { // We are clear to publish the stream to the session. this.session.publish(this.publisher);
--- a/browser/components/loop/content/shared/js/utils.js +++ b/browser/components/loop/content/shared/js/utils.js @@ -12,16 +12,24 @@ loop.shared.utils = (function(mozL10n) { /** * Call types used for determining if a call is audio/video or audio-only. */ var CALL_TYPES = { AUDIO_VIDEO: "audio-video", AUDIO_ONLY: "audio" }; + var FAILURE_REASONS = { + MEDIA_DENIED: "reason-media-denied", + COULD_NOT_CONNECT: "reason-could-not-connect", + NETWORK_DISCONNECTED: "reason-network-disconnected", + EXPIRED_OR_INVALID: "reason-expired-or-invalid", + UNKNOWN: "reason-unknown" + }; + /** * Format a given date into an l10n-friendly string. * * @param {Integer} The timestamp in seconds to format. * @return {String} The formatted string. */ function formatDate(timestamp) { var date = (new Date(timestamp * 1000)); @@ -105,14 +113,15 @@ loop.shared.utils = (function(mozL10n) { learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl") }), recipient ); } return { CALL_TYPES: CALL_TYPES, + FAILURE_REASONS: FAILURE_REASONS, Helper: Helper, composeCallUrlEmail: composeCallUrlEmail, formatDate: formatDate, getBoolPreference: getBoolPreference }; })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js @@ -6,16 +6,17 @@ /* global loop:true, React */ /* jshint newcap:false, maxlen:false */ var loop = loop || {}; loop.standaloneRoomViews = (function(mozL10n) { "use strict"; + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; var ROOM_STATES = loop.store.ROOM_STATES; var sharedActions = loop.shared.actions; var sharedMixins = loop.shared.mixins; var sharedViews = loop.shared.views; var StandaloneRoomInfoArea = React.createClass({displayName: 'StandaloneRoomInfoArea', propTypes: { helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired @@ -35,16 +36,30 @@ loop.standaloneRoomViews = (function(moz React.DOM.a({href: loop.config.brandWebsiteUrl, className: "btn btn-info"}, mozL10n.get("rooms_room_full_call_to_action_nonFx_label", { brandShortname: mozL10n.get("brandShortname") }) ) ); }, + /** + * @return String An appropriate string according to the failureReason. + */ + _getFailureString: function() { + switch(this.props.failureReason) { + case FAILURE_REASONS.MEDIA_DENIED: + return mozL10n.get("rooms_media_denied_message"); + case FAILURE_REASONS.EXPIRED_OR_INVALID: + return mozL10n.get("rooms_unavailable_notification_message"); + default: + return mozL10n.get("status_error"); + }; + }, + _renderContent: function() { switch(this.props.roomState) { case ROOM_STATES.INIT: case ROOM_STATES.READY: { return ( React.DOM.button({className: "btn btn-join btn-info", onClick: this.props.joinRoom}, mozL10n.get("rooms_room_join_label") @@ -63,16 +78,22 @@ loop.standaloneRoomViews = (function(moz return ( React.DOM.div(null, React.DOM.p({className: "full-room-message"}, mozL10n.get("rooms_room_full_label") ), React.DOM.p(null, this._renderCallToActionLink()) ) ); + case ROOM_STATES.FAILED: + return ( + React.DOM.p({className: "failed-room-message"}, + this._getFailureString() + ) + ); default: return null; } }, render: function() { return ( React.DOM.div({className: "room-inner-info-area"}, @@ -247,16 +268,17 @@ loop.standaloneRoomViews = (function(moz "local-stream": true, "local-stream-audio": false }); return ( React.DOM.div({className: "room-conversation-wrapper"}, StandaloneRoomHeader(null), StandaloneRoomInfoArea({roomState: this.state.roomState, + failureReason: this.state.failureReason, joinRoom: this.joinRoom, helper: this.props.helper}), React.DOM.div({className: "video-layout-wrapper"}, React.DOM.div({className: "conversation room-conversation"}, React.DOM.h2({className: "room-name"}, this.state.roomName), React.DOM.div({className: "media nested"}, React.DOM.div({className: "video_wrapper remote_wrapper"}, React.DOM.div({className: "video_inner remote"})
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx @@ -6,16 +6,17 @@ /* global loop:true, React */ /* jshint newcap:false, maxlen:false */ var loop = loop || {}; loop.standaloneRoomViews = (function(mozL10n) { "use strict"; + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; var ROOM_STATES = loop.store.ROOM_STATES; var sharedActions = loop.shared.actions; var sharedMixins = loop.shared.mixins; var sharedViews = loop.shared.views; var StandaloneRoomInfoArea = React.createClass({ propTypes: { helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired @@ -35,16 +36,30 @@ loop.standaloneRoomViews = (function(moz <a href={loop.config.brandWebsiteUrl} className="btn btn-info"> {mozL10n.get("rooms_room_full_call_to_action_nonFx_label", { brandShortname: mozL10n.get("brandShortname") })} </a> ); }, + /** + * @return String An appropriate string according to the failureReason. + */ + _getFailureString: function() { + switch(this.props.failureReason) { + case FAILURE_REASONS.MEDIA_DENIED: + return mozL10n.get("rooms_media_denied_message"); + case FAILURE_REASONS.EXPIRED_OR_INVALID: + return mozL10n.get("rooms_unavailable_notification_message"); + default: + return mozL10n.get("status_error"); + }; + }, + _renderContent: function() { switch(this.props.roomState) { case ROOM_STATES.INIT: case ROOM_STATES.READY: { return ( <button className="btn btn-join btn-info" onClick={this.props.joinRoom}> {mozL10n.get("rooms_room_join_label")} @@ -63,16 +78,22 @@ loop.standaloneRoomViews = (function(moz return ( <div> <p className="full-room-message"> {mozL10n.get("rooms_room_full_label")} </p> <p>{this._renderCallToActionLink()}</p> </div> ); + case ROOM_STATES.FAILED: + return ( + <p className="failed-room-message"> + {this._getFailureString()} + </p> + ); default: return null; } }, render: function() { return ( <div className="room-inner-info-area"> @@ -247,16 +268,17 @@ loop.standaloneRoomViews = (function(moz "local-stream": true, "local-stream-audio": false }); return ( <div className="room-conversation-wrapper"> <StandaloneRoomHeader /> <StandaloneRoomInfoArea roomState={this.state.roomState} + failureReason={this.state.failureReason} joinRoom={this.joinRoom} helper={this.props.helper} /> <div className="video-layout-wrapper"> <div className="conversation room-conversation"> <h2 className="room-name">{this.state.roomName}</h2> <div className="media nested"> <div className="video_wrapper remote_wrapper"> <div className="video_inner remote"></div>
--- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties +++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties @@ -111,16 +111,18 @@ rooms_new_room_button_label=Start a conv rooms_only_occupant_label=You're the first one here. rooms_panel_title=Choose a conversation or start a new one rooms_room_full_label=There are already two people in this conversation. rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} » rooms_room_joined_label=Someone has joined the conversation! rooms_room_join_label=Join the conversation rooms_display_name_guest=Guest +rooms_unavailable_notification_message=Sorry, you cannot join this conversation. The link may be expired or invalid. +rooms_media_denied_message=We could not get access to your microphone or camera. Please reload the page to try again. ## LOCALIZATION_NOTE(standalone_title_with_status): {{clientShortname}} will be ## replaced by the brand name and {{currentStatus}} will be replaced ## by the current call status (Connecting, Ringing, etc.) standalone_title_with_status={{clientShortname}} — {{currentStatus}} status_in_conversation=In conversation status_conversation_ended=Conversation ended status_error=Something went wrong
--- a/browser/components/loop/test/desktop-local/roomViews_test.js +++ b/browser/components/loop/test/desktop-local/roomViews_test.js @@ -60,16 +60,17 @@ describe("loop.roomViews", function () { var testView = TestUtils.renderIntoDocument(TestView({ roomStore: roomStore })); expect(testView.state).eql({ roomState: ROOM_STATES.INIT, audioMuted: false, videoMuted: false, + failureReason: undefined foo: "bar" }); }); it("should listen to store changes", function() { var TestView = React.createClass({ mixins: [loop.roomViews.ActiveRoomStoreMixin], render: function() { return React.DOM.div(); }
--- a/browser/components/loop/test/shared/activeRoomStore_test.js +++ b/browser/components/loop/test/shared/activeRoomStore_test.js @@ -1,17 +1,19 @@ /* global chai, loop */ var expect = chai.expect; var sharedActions = loop.shared.actions; describe("loop.store.ActiveRoomStore", function () { "use strict"; + var SERVER_CODES = loop.store.SERVER_CODES; var ROOM_STATES = loop.store.ROOM_STATES; + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver; var fakeMultiplexGum; beforeEach(function() { sandbox = sinon.sandbox.create(); sandbox.useFakeTimers(); dispatcher = new loop.Dispatcher(); @@ -86,29 +88,50 @@ describe("loop.store.ActiveRoomStore", f it("should log the error", function() { store.roomFailure({error: fakeError}); sinon.assert.calledOnce(console.error); sinon.assert.calledWith(console.error, sinon.match(ROOM_STATES.READY), fakeError); }); - it("should set the state to `FULL` on server errno 202", function() { - fakeError.errno = 202; + it("should set the state to `FULL` on server error room full", function() { + fakeError.errno = SERVER_CODES.ROOM_FULL; store.roomFailure({error: fakeError}); expect(store._storeState.roomState).eql(ROOM_STATES.FULL); }); it("should set the state to `FAILED` on generic error", function() { store.roomFailure({error: fakeError}); expect(store._storeState.roomState).eql(ROOM_STATES.FAILED); + expect(store._storeState.failureReason).eql(FAILURE_REASONS.UNKNOWN); }); + + it("should set the failureReason to EXPIRED_OR_INVALID on server error: " + + "invalid token", function() { + fakeError.errno = SERVER_CODES.INVALID_TOKEN; + + store.roomFailure({error: fakeError}); + + expect(store._storeState.roomState).eql(ROOM_STATES.FAILED); + expect(store._storeState.failureReason).eql(FAILURE_REASONS.EXPIRED_OR_INVALID); + }); + + it("should set the failureReason to EXPIRED_OR_INVALID on server error: " + + "expired", function() { + fakeError.errno = SERVER_CODES.EXPIRED; + + store.roomFailure({error: fakeError}); + + expect(store._storeState.roomState).eql(ROOM_STATES.FAILED); + expect(store._storeState.failureReason).eql(FAILURE_REASONS.EXPIRED_OR_INVALID); + }); }); describe("#setupWindowData", function() { var fakeToken, fakeRoomData; beforeEach(function() { fakeToken = "337-ff-54"; fakeRoomData = { @@ -239,16 +262,24 @@ describe("loop.store.ActiveRoomStore", f }); }); describe("#joinRoom", function() { beforeEach(function() { store.setStoreState({roomToken: "tokenFake"}); }); + it("should reset failureReason", function() { + store.setStoreState({failureReason: "Test"}); + + store.joinRoom(); + + expect(store.getStoreState().failureReason).eql(undefined); + }); + it("should call rooms.join on mozLoop", function() { store.joinRoom(); sinon.assert.calledOnce(fakeMozLoop.rooms.join); sinon.assert.calledWith(fakeMozLoop.rooms.join, "tokenFake"); }); it("should dispatch `JoinedRoom` on success", function() { @@ -375,55 +406,67 @@ describe("loop.store.ActiveRoomStore", f it("should set the state to `SESSION_CONNECTED`", function() { store.connectedToSdkServers(new sharedActions.ConnectedToSdkServers()); expect(store.getStoreState().roomState).eql(ROOM_STATES.SESSION_CONNECTED); }); }); describe("#connectionFailure", function() { + var connectionFailureAction; + beforeEach(function() { store.setStoreState({ roomState: ROOM_STATES.JOINED, roomToken: "fakeToken", sessionToken: "1627384950" }); + + connectionFailureAction = new sharedActions.ConnectionFailure({ + reason: "FAIL" + }); + }); + + it("should store the failure reason", function() { + store.connectionFailure(connectionFailureAction); + + expect(store.getStoreState().failureReason).eql("FAIL"); }); it("should reset the multiplexGum", function() { - store.leaveRoom(); + store.connectionFailure(connectionFailureAction); sinon.assert.calledOnce(fakeMultiplexGum.reset); }); it("should disconnect from the servers via the sdk", function() { - store.connectionFailure(); + store.connectionFailure(connectionFailureAction); sinon.assert.calledOnce(fakeSdkDriver.disconnectSession); }); it("should clear any existing timeout", function() { sandbox.stub(window, "clearTimeout"); store._timeout = {}; - store.connectionFailure(); + store.connectionFailure(connectionFailureAction); sinon.assert.calledOnce(clearTimeout); }); it("should call mozLoop.rooms.leave", function() { - store.connectionFailure(); + store.connectionFailure(connectionFailureAction); sinon.assert.calledOnce(fakeMozLoop.rooms.leave); sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave, "fakeToken", "1627384950"); }); it("should set the state to `FAILED`", function() { - store.connectionFailure(); + store.connectionFailure(connectionFailureAction); expect(store.getStoreState().roomState).eql(ROOM_STATES.FAILED); }); }); describe("#setMute", function() { it("should save the mute state for the audio stream", function() { store.setStoreState({audioMuted: false});
--- a/browser/components/loop/test/shared/otSdkDriver_test.js +++ b/browser/components/loop/test/shared/otSdkDriver_test.js @@ -2,26 +2,29 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ var expect = chai.expect; describe("loop.OTSdkDriver", function () { "use strict"; var sharedActions = loop.shared.actions; - + var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS; var sandbox; var dispatcher, driver, publisher, sdk, session, sessionData; - var fakeLocalElement, fakeRemoteElement, publisherConfig; + var fakeLocalElement, fakeRemoteElement, publisherConfig, fakeEvent; beforeEach(function() { sandbox = sinon.sandbox.create(); fakeLocalElement = {fake: 1}; fakeRemoteElement = {fake: 2}; + fakeEvent = { + preventDefault: sinon.stub() + }; publisherConfig = { fake: "config" }; sessionData = { apiKey: "1234567890", sessionId: "3216549870", sessionToken: "1357924680" }; @@ -29,24 +32,24 @@ describe("loop.OTSdkDriver", function () dispatcher = new loop.Dispatcher(); session = _.extend({ connect: sinon.stub(), disconnect: sinon.stub(), publish: sinon.stub(), subscribe: sinon.stub() }, Backbone.Events); - publisher = { + publisher = _.extend({ destroy: sinon.stub(), publishAudio: sinon.stub(), publishVideo: sinon.stub() - }; + }, Backbone.Events); sdk = { - initPublisher: sinon.stub(), + initPublisher: sinon.stub().returns(publisher), initSession: sinon.stub().returns(session) }; driver = new loop.OTSdkDriver({ dispatcher: dispatcher, sdk: sdk }); }); @@ -75,61 +78,16 @@ describe("loop.OTSdkDriver", function () getLocalElementFunc: function() {return fakeLocalElement;}, getRemoteElementFunc: function() {return fakeRemoteElement;}, publisherConfig: publisherConfig })); sinon.assert.calledOnce(sdk.initPublisher); sinon.assert.calledWith(sdk.initPublisher, fakeLocalElement, publisherConfig); }); - - describe("On Publisher Complete", function() { - it("should publish the stream if the connection is ready", function() { - sdk.initPublisher.callsArgWith(2, null); - - driver.session = session; - driver._sessionConnected = true; - - dispatcher.dispatch(new sharedActions.SetupStreamElements({ - getLocalElementFunc: function() {return fakeLocalElement;}, - getRemoteElementFunc: function() {return fakeRemoteElement;}, - publisherConfig: publisherConfig - })); - - sinon.assert.calledOnce(session.publish); - }); - - it("should dispatch connectionFailure if connecting failed", function() { - sdk.initPublisher.callsArgWith(2, new Error("Failure")); - - // Special stub, as we want to use the dispatcher, but also know that - // we've been called correctly for the second dispatch. - var dispatchStub = (function() { - var originalDispatch = dispatcher.dispatch.bind(dispatcher); - return sandbox.stub(dispatcher, "dispatch", function(action) { - originalDispatch(action); - }); - }()); - - driver.session = session; - driver._sessionConnected = true; - - dispatcher.dispatch(new sharedActions.SetupStreamElements({ - getLocalElementFunc: function() {return fakeLocalElement;}, - getRemoteElementFunc: function() {return fakeRemoteElement;}, - publisherConfig: publisherConfig - })); - - sinon.assert.called(dispatcher.dispatch); - sinon.assert.calledWithMatch(dispatcher.dispatch, - sinon.match.hasOwn("name", "connectionFailure")); - sinon.assert.calledWithMatch(dispatcher.dispatch, - sinon.match.hasOwn("reason", "noMedia")); - }); - }); }); describe("#setMute", function() { beforeEach(function() { sdk.initPublisher.returns(publisher); dispatcher.dispatch(new sharedActions.SetupStreamElements({ getLocalElementFunc: function() {return fakeLocalElement;}, @@ -189,17 +147,17 @@ describe("loop.OTSdkDriver", function () sandbox.stub(dispatcher, "dispatch"); driver.connectSession(sessionData); sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledWithMatch(dispatcher.dispatch, sinon.match.hasOwn("name", "connectionFailure")); sinon.assert.calledWithMatch(dispatcher.dispatch, - sinon.match.hasOwn("reason", "couldNotConnect")); + sinon.match.hasOwn("reason", FAILURE_REASONS.COULD_NOT_CONNECT)); }); }); }); describe("#disconnectionSession", function() { it("should disconnect the session", function() { driver.session = session; @@ -264,17 +222,17 @@ describe("loop.OTSdkDriver", function () session.trigger("sessionDisconnected", { reason: "networkDisconnected" }); sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledWithMatch(dispatcher.dispatch, sinon.match.hasOwn("name", "connectionFailure")); sinon.assert.calledWithMatch(dispatcher.dispatch, - sinon.match.hasOwn("reason", "networkDisconnected")); + sinon.match.hasOwn("reason", FAILURE_REASONS.NETWORK_DISCONNECTED)); }); }); describe("streamCreated", function() { var fakeStream; beforeEach(function() { fakeStream = { @@ -323,10 +281,46 @@ describe("loop.OTSdkDriver", function () function() { session.trigger("connectionCreated", { connection: {id: "localUser"} }); sinon.assert.notCalled(dispatcher.dispatch); }); }); + + describe("accessAllowed", function() { + it("should publish the stream if the connection is ready", function() { + driver._sessionConnected = true; + + publisher.trigger("accessAllowed", fakeEvent); + + sinon.assert.calledOnce(session.publish); + }); + }); + + describe("accessDenied", function() { + it("should prevent the default event behavior", function() { + publisher.trigger("accessDenied", fakeEvent); + + sinon.assert.calledOnce(fakeEvent.preventDefault); + }); + + it("should dispatch connectionFailure", function() { + publisher.trigger("accessDenied", fakeEvent); + + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithMatch(dispatcher.dispatch, + sinon.match.hasOwn("name", "connectionFailure")); + sinon.assert.calledWithMatch(dispatcher.dispatch, + sinon.match.hasOwn("reason", FAILURE_REASONS.MEDIA_DENIED)); + }); + }); + + describe("accessDialogOpened", function() { + it("should prevent the default event behavior", function() { + publisher.trigger("accessDialogOpened", fakeEvent); + + sinon.assert.calledOnce(fakeEvent.preventDefault); + }); + }); }); });
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js +++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js @@ -134,16 +134,26 @@ describe("loop.standaloneRoomViews", fun function() { activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL}); expect(view.getDOMNode().querySelector(".full-room-message")) .not.eql(null); }); }); + describe("Failed room message", function() { + it("should display a failed room message on FAILED", + function() { + activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED}); + + expect(view.getDOMNode().querySelector(".failed-room-message")) + .not.eql(null); + }); + }); + describe("Join button", function() { function getJoinButton(view) { return view.getDOMNode().querySelector(".btn-join"); } it("should render the Join button when room isn't active", function() { activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
--- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -603,16 +603,26 @@ Example({summary: "Standalone room conversation (full - non FFx user)"}, React.DOM.div({className: "standalone"}, StandaloneRoomView({ dispatcher: dispatcher, activeRoomStore: activeRoomStore, roomState: ROOM_STATES.FULL, helper: {isFirefox: returnFalse}}) ) + ), + + Example({summary: "Standalone room conversation (failed)"}, + React.DOM.div({className: "standalone"}, + StandaloneRoomView({ + dispatcher: dispatcher, + activeRoomStore: activeRoomStore, + roomState: ROOM_STATES.FAILED, + helper: {isFirefox: returnFalse}}) + ) ) ), Section({name: "SVG icons preview"}, Example({summary: "16x16"}, SVGIcons(null) ) )
--- a/browser/components/loop/ui/ui-showcase.jsx +++ b/browser/components/loop/ui/ui-showcase.jsx @@ -604,16 +604,26 @@ <div className="standalone"> <StandaloneRoomView dispatcher={dispatcher} activeRoomStore={activeRoomStore} roomState={ROOM_STATES.FULL} helper={{isFirefox: returnFalse}} /> </div> </Example> + + <Example summary="Standalone room conversation (failed)"> + <div className="standalone"> + <StandaloneRoomView + dispatcher={dispatcher} + activeRoomStore={activeRoomStore} + roomState={ROOM_STATES.FAILED} + helper={{isFirefox: returnFalse}} /> + </div> + </Example> </Section> <Section name="SVG icons preview"> <Example summary="16x16"> <SVGIcons /> </Example> </Section>