--- a/browser/components/loop/standalone/content/js/standaloneMetricsStore.js
+++ b/browser/components/loop/standalone/content/js/standaloneMetricsStore.js
@@ -45,17 +45,18 @@ loop.store.StandaloneMetricsStore = (fun
"connectionFailure",
"gotMediaPermission",
"joinRoom",
"joinedRoom",
"leaveRoom",
"mediaConnected",
"recordClick",
"remotePeerConnected",
- "retryAfterRoomFailure"
+ "retryAfterRoomFailure",
+ "windowUnload"
],
/**
* Initializes the store and starts listening to the activeRoomStore.
*
* @param {Object} options Options for the store, should include a
* reference to the activeRoomStore.
*/
@@ -252,13 +253,24 @@ loop.store.StandaloneMetricsStore = (fun
return;
}
this._storeState[muteItem] = muted;
var muteType = type === "audio" ? METRICS_GA_ACTIONS.audioMute : METRICS_GA_ACTIONS.faceMute;
var muteState = muted ? "mute" : "unmute";
this._storeEvent(METRICS_GA_CATEGORY.general, muteType, muteState);
+ },
+
+ /**
+ * Called when the window is unloaded, either by code, or by the user
+ * explicitly closing it. Expected to do any necessary housekeeping, such
+ * as shutting down the call cleanly and adding any relevant telemetry data.
+ */
+ windowUnload: function() {
+ if (this.activeRoomStore) {
+ this.stopListening(this.activeRoomStore);
+ }
}
});
return StandaloneMetricsStore;
})();
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -409,16 +409,18 @@ loop.standaloneRoomViews = (function(moz
case ROOM_STATES.JOINED:
case ROOM_STATES.MEDIA_WAIT:
// this case is so that we don't show an avatar while waiting for
// the other party to connect
return true;
case ROOM_STATES.FAILED:
case ROOM_STATES.CLOSING:
+ case ROOM_STATES.FULL:
+ case ROOM_STATES.ENDED:
// the other person has shown up, so we don't want to show an avatar
return true;
default:
console.warn("StandaloneRoomView.shouldRenderRemoteVideo:" +
" unexpected roomState: ", this.state.roomState);
return true;
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -409,16 +409,18 @@ loop.standaloneRoomViews = (function(moz
case ROOM_STATES.JOINED:
case ROOM_STATES.MEDIA_WAIT:
// this case is so that we don't show an avatar while waiting for
// the other party to connect
return true;
case ROOM_STATES.FAILED:
case ROOM_STATES.CLOSING:
+ case ROOM_STATES.FULL:
+ case ROOM_STATES.ENDED:
// the other person has shown up, so we don't want to show an avatar
return true;
default:
console.warn("StandaloneRoomView.shouldRenderRemoteVideo:" +
" unexpected roomState: ", this.state.roomState);
return true;
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -88,17 +88,17 @@ loop.webapp = (function($, _, OT, mozL10
*/
var PromoteFirefoxView = React.createClass({displayName: "PromoteFirefoxView",
propTypes: {
isFirefox: React.PropTypes.bool.isRequired
},
render: function() {
if (this.props.isFirefox) {
- return React.createElement("div", null);
+ return null;
}
return (
React.createElement("div", {className: "promote-firefox"},
React.createElement("h3", null, mozL10n.get("promote_firefox_hello_heading", {brandShortname: mozL10n.get("brandShortname")})),
React.createElement("p", null,
React.createElement("a", {className: "btn btn-large btn-accept",
href: loop.config.downloadFirefoxUrl},
mozL10n.get("get_firefox_button", {
@@ -644,18 +644,18 @@ loop.webapp = (function($, _, OT, mozL10
var OutgoingConversationView = React.createClass({displayName: "OutgoingConversationView",
propTypes: {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(sharedModels.ConversationModel),
React.PropTypes.instanceOf(FxOSConversationModel)
]).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
- notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
- .isRequired,
+ isFirefox: React.PropTypes.bool.isRequired,
+ notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection).isRequired,
sdk: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
callStatus: "start"
};
},
@@ -733,17 +733,17 @@ loop.webapp = (function($, _, OT, mozL10
conversation: this.props.conversation,
dispatcher: this.props.dispatcher,
onAfterFeedbackReceived: this.resetCallStatus(),
sdk: this.props.sdk})
);
}
case "expired": {
return (
- React.createElement(CallUrlExpiredView, null)
+ React.createElement(CallUrlExpiredView, {isFirefox: this.props.isFirefox})
);
}
default: {
return React.createElement(HomeView, null);
}
}
},
@@ -981,16 +981,17 @@ loop.webapp = (function($, _, OT, mozL10
return React.createElement(UnsupportedBrowserView, {isFirefox: this.state.isFirefox});
}
case "outgoing": {
return (
React.createElement(OutgoingConversationView, {
client: this.props.client,
conversation: this.props.conversation,
dispatcher: this.props.dispatcher,
+ isFirefox: this.state.isFirefox,
notifications: this.props.notifications,
sdk: this.props.sdk})
);
}
case "room": {
return (
React.createElement(loop.standaloneRoomViews.StandaloneRoomView, {
activeRoomStore: this.props.activeRoomStore,
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -88,17 +88,17 @@ loop.webapp = (function($, _, OT, mozL10
*/
var PromoteFirefoxView = React.createClass({
propTypes: {
isFirefox: React.PropTypes.bool.isRequired
},
render: function() {
if (this.props.isFirefox) {
- return <div />;
+ return null;
}
return (
<div className="promote-firefox">
<h3>{mozL10n.get("promote_firefox_hello_heading", {brandShortname: mozL10n.get("brandShortname")})}</h3>
<p>
<a className="btn btn-large btn-accept"
href={loop.config.downloadFirefoxUrl}>
{mozL10n.get("get_firefox_button", {
@@ -644,18 +644,18 @@ loop.webapp = (function($, _, OT, mozL10
var OutgoingConversationView = React.createClass({
propTypes: {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(sharedModels.ConversationModel),
React.PropTypes.instanceOf(FxOSConversationModel)
]).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
- notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
- .isRequired,
+ isFirefox: React.PropTypes.bool.isRequired,
+ notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection).isRequired,
sdk: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
callStatus: "start"
};
},
@@ -733,17 +733,17 @@ loop.webapp = (function($, _, OT, mozL10
conversation={this.props.conversation}
dispatcher={this.props.dispatcher}
onAfterFeedbackReceived={this.resetCallStatus()}
sdk={this.props.sdk} />
);
}
case "expired": {
return (
- <CallUrlExpiredView />
+ <CallUrlExpiredView isFirefox={this.props.isFirefox}/>
);
}
default: {
return <HomeView />;
}
}
},
@@ -981,16 +981,17 @@ loop.webapp = (function($, _, OT, mozL10
return <UnsupportedBrowserView isFirefox={this.state.isFirefox}/>;
}
case "outgoing": {
return (
<OutgoingConversationView
client={this.props.client}
conversation={this.props.conversation}
dispatcher={this.props.dispatcher}
+ isFirefox={this.state.isFirefox}
notifications={this.props.notifications}
sdk={this.props.sdk} />
);
}
case "room": {
return (
<loop.standaloneRoomViews.StandaloneRoomView
activeRoomStore={this.props.activeRoomStore}
--- a/browser/components/loop/test/index.html
+++ b/browser/components/loop/test/index.html
@@ -9,11 +9,12 @@
</head>
<body>
<h1>Loop tests</h1>
<ul>
<li><a href="shared/">Shared tests</a></li>
<li><a href="desktop-local/">Local tests</a></li>
<li><a href="standalone/">Standalone tests</a></li>
<li><a href="coverage/">Code Coverage</a></li>
+ <li><a href="../ui/">UI Showcase</a></li>
</ul>
</body>
</html>
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -79,18 +79,18 @@
<script>
describe("Uncaught Error Check", function() {
it("should load the tests without errors", function() {
chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
});
});
describe("Unexpected Warnings Check", function() {
- it("should long only the warnings we expect", function() {
- chai.expect(caughtWarnings.length).to.eql(10);
+ it("should log only the warnings we expect", function() {
+ chai.expect(caughtWarnings.length).to.eql(0);
});
});
mocha.run(function () {
var completeNode = document.createElement("p");
completeNode.setAttribute("id", "complete");
completeNode.appendChild(document.createTextNode("Complete"));
document.getElementById("mocha").appendChild(completeNode);
--- a/browser/components/loop/test/standalone/standaloneMetricsStore_test.js
+++ b/browser/components/loop/test/standalone/standaloneMetricsStore_test.js
@@ -190,10 +190,27 @@ describe("loop.store.StandaloneMetricsSt
audioMuted: true
});
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.audioMute,
"mute");
});
+
+ describe("Event listeners", function() {
+ it("should call windowUnload when action is dispatched", function() {
+ sandbox.stub(store, "windowUnload");
+
+ dispatcher.dispatch(new sharedActions.WindowUnload());
+ sinon.assert.calledOnce(store.windowUnload);
+ });
+
+ it("should stop listening to activeRoomStore", function() {
+ var stopListeningStub = sandbox.stub(store, "stopListening");
+ store.windowUnload();
+
+ sinon.assert.calledOnce(stopListeningStub);
+ sinon.assert.calledWithExactly(stopListeningStub, store.activeRoomStore);
+ });
+ });
});
});
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -12,26 +12,31 @@ describe("loop.webapp", function() {
sharedViews = loop.shared.views,
sharedUtils = loop.shared.utils,
standaloneMedia = loop.standaloneMedia,
sandbox,
notifications,
stubGetPermsAndCacheMedia,
fakeAudioXHR,
dispatcher,
+ mozL10nGet,
WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
notifications = new sharedModels.NotificationCollection();
stubGetPermsAndCacheMedia = sandbox.stub(
loop.standaloneMedia._MultiplexGum.prototype, "getPermsAndCacheMedia");
+ mozL10nGet = sandbox.stub(navigator.mozL10n, "get", function(x) {
+ return "translated:" + x;
+ });
+
fakeAudioXHR = {
open: sinon.spy(),
send: function() {},
abort: function() {},
getResponseHeader: function(header) {
if (header === "Content-Type") {
return "audio/ogg";
}
@@ -109,16 +114,17 @@ describe("loop.webapp", function() {
sandbox.stub(client, "requestCallUrlInfo");
conversation = new sharedModels.ConversationModel({}, {
sdk: {}
});
conversation.set("loopToken", "fakeToken");
ocView = mountTestComponent({
client: client,
conversation: conversation,
+ isFirefox: true,
notifications: notifications,
sdk: {
on: sandbox.stub()
},
dispatcher: dispatcher
});
});
@@ -672,56 +678,55 @@ describe("loop.webapp", function() {
});
// Stub this to stop the StartConversationView kicking in the request and
// follow-ups.
sandbox.stub(client, "requestCallUrlInfo");
});
it("should display the UnsupportedDeviceView for `unsupportedDevice` window type",
function() {
- standaloneAppStore.setStoreState({windowType: "unsupportedDevice"});
-
+ standaloneAppStore.setStoreState({windowType: "unsupportedDevice", unsupportedPlatform: "ios"});
var webappRootView = mountTestComponent();
TestUtils.findRenderedComponentWithType(webappRootView,
loop.webapp.UnsupportedDeviceView);
});
it("should display the UnsupportedBrowserView for `unsupportedBrowser` window type",
function() {
- standaloneAppStore.setStoreState({windowType: "unsupportedBrowser"});
+ standaloneAppStore.setStoreState({windowType: "unsupportedBrowser", isFirefox: false});
var webappRootView = mountTestComponent();
TestUtils.findRenderedComponentWithType(webappRootView,
loop.webapp.UnsupportedBrowserView);
});
it("should display the OutgoingConversationView for `outgoing` window type",
function() {
- standaloneAppStore.setStoreState({windowType: "outgoing"});
+ standaloneAppStore.setStoreState({windowType: "outgoing", isFirefox: true});
var webappRootView = mountTestComponent();
TestUtils.findRenderedComponentWithType(webappRootView,
loop.webapp.OutgoingConversationView);
});
it("should display the StandaloneRoomView for `room` window type",
function() {
- standaloneAppStore.setStoreState({windowType: "room"});
+ standaloneAppStore.setStoreState({windowType: "room", isFirefox: true});
var webappRootView = mountTestComponent();
TestUtils.findRenderedComponentWithType(webappRootView,
loop.standaloneRoomViews.StandaloneRoomView);
});
it("should display the HomeView for `home` window type", function() {
- standaloneAppStore.setStoreState({windowType: "home"});
+ standaloneAppStore.setStoreState({windowType: "home", isFirefox: true});
var webappRootView = mountTestComponent();
TestUtils.findRenderedComponentWithType(webappRootView,
loop.webapp.HomeView);
});
});
@@ -1085,29 +1090,29 @@ describe("loop.webapp", function() {
});
describe("PromoteFirefoxView", function() {
describe("#render", function() {
it("should not render when using Firefox", function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(loop.webapp.PromoteFirefoxView, {
isFirefox: true
- }));
+ }));
- expect(comp.getDOMNode().querySelectorAll("h3").length).eql(0);
+ expect(comp.getDOMNode()).eql(null);
});
it("should render when not using Firefox", function() {
var comp = TestUtils.renderIntoDocument(
- React.createElement(
- loop.webapp.PromoteFirefoxView, {
+ React.createElement(loop.webapp.PromoteFirefoxView, {
isFirefox: false
- }));
+ }));
- expect(comp.getDOMNode().querySelectorAll("h3").length).eql(1);
+ sinon.assert.calledWith(mozL10nGet, "promote_firefox_hello_heading");
+ sinon.assert.calledWith(mozL10nGet, "get_firefox_button");
});
});
});
describe("Firefox OS", function() {
var conversation, client;
before(function() {
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -1536,17 +1536,17 @@
setTimeout(waitForQueuedFrames, 500);
return;
}
// Put the title back, in case views changed it.
document.title = "Loop UI Components Showcase";
// This simulates the mocha layout for errors which means we can run
// this alongside our other unit tests but use the same harness.
- var expectedWarningsCount = 18;
+ var expectedWarningsCount = 16;
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
if (uncaughtError || warningsMismatch) {
$("#results").append("<div class='failures'><em>" +
((uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>");
if (warningsMismatch) {
$("#results").append("<li class='test fail'>" +
"<h2>Unexpected number of warnings detected in UI-Showcase</h2>" +
"<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -1536,17 +1536,17 @@
setTimeout(waitForQueuedFrames, 500);
return;
}
// Put the title back, in case views changed it.
document.title = "Loop UI Components Showcase";
// This simulates the mocha layout for errors which means we can run
// this alongside our other unit tests but use the same harness.
- var expectedWarningsCount = 18;
+ var expectedWarningsCount = 16;
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
if (uncaughtError || warningsMismatch) {
$("#results").append("<div class='failures'><em>" +
((uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>");
if (warningsMismatch) {
$("#results").append("<li class='test fail'>" +
"<h2>Unexpected number of warnings detected in UI-Showcase</h2>" +
"<pre class='error'>Got: " + caughtWarnings.length + "\n" +