Bug 1132222 - Add more metrics reporting to the Loop standalone UI. r=dmose

This commit is contained in:
Mark Banner 2015-04-22 08:51:46 +01:00
parent c9e64b58de
commit ed049b31db
10 changed files with 529 additions and 13 deletions

View File

@ -468,6 +468,17 @@ loop.shared.actions = (function() {
LeaveRoom: Action.define("leaveRoom", {
}),
/**
* Used to record a link click for metrics purposes.
*/
RecordClick: Action.define("recordClick", {
// Note: for ToS and Privacy links, this should be the link, for
// other links this should be a generic description so that we don't
// record what users are clicking, just the information about the fact
// they clicked the link in that spot (e.g. "Shared URL").
linkInfo: String
}),
/**
* Requires detailed information on sad feedback.
*/

View File

@ -29,6 +29,7 @@
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-36116321-15', 'auto');
ga('set', 'anonymizeIp', true);
ga('send', 'pageview');
}
</script>
@ -129,6 +130,7 @@
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
<script type="text/javascript" src="js/fxOSMarketplace.js"></script>
<script type="text/javascript" src="js/standaloneRoomViews.js"></script>
<script type="text/javascript" src="js/standaloneMetricsStore.js"></script>
<script type="text/javascript" src="js/webapp.js"></script>
<script>

View File

@ -0,0 +1,210 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.store = loop.store || {};
/**
* The standalone metrics store is used to log activities to
* analytics.
*
* Where possible we log events via receiving actions. However, some
* combinations of actions and events require us to listen directly to
* changes in the activeRoomStore so that we gain the benefit of the logic
* in that store.
*/
loop.store.StandaloneMetricsStore = (function() {
"use strict";
var ROOM_STATES = loop.store.ROOM_STATES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
loop.store.metrics = loop.store.metrics || {};
var METRICS_GA_CATEGORY = loop.store.METRICS_GA_CATEGORY = {
general: "/conversation/ interactions",
download: "Firefox Downloads"
};
var METRICS_GA_ACTIONS = loop.store.METRICS_GA_ACTIONS = {
audioMute: "audio mute",
button: "button click",
download: "download button click",
faceMute: "face mute",
link: "link click",
pageLoad: "page load messages",
success: "success",
support: "support link click"
}
var StandaloneMetricsStore = loop.store.createStore({
actions: [
"gotMediaPermission",
"joinRoom",
"leaveRoom",
"mediaConnected",
"recordClick"
],
/**
* Initializes the store and starts listening to the activeRoomStore.
*
* @param {Object} options Options for the store, should include a
* reference to the activeRoomStore.
*/
initialize: function(options) {
options = options || {};
if (!options.activeRoomStore) {
throw new Error("Missing option activeRoomStore");
}
// Don't bother listening if we're not storing metrics.
// I'd love for ga to be an option, but that messes up the function
// working properly.
if (window.ga) {
this.activeRoomStore = options.activeRoomStore;
this.listenTo(options.activeRoomStore, "change",
this._onActiveRoomStoreChange.bind(this));
}
},
/**
* Returns initial state data for this store. These are mainly reflections
* of activeRoomStore so we can match initial states for change tracking
* purposes.
*
* @return {Object} The initial store state.
*/
getInitialStoreState: function() {
return {
audioMuted: false,
roomState: ROOM_STATES.INIT,
videoMuted: false
};
},
/**
* Saves an event to ga.
*
* @param {String} category The category for the event.
* @param {String} action The type of action.
* @param {String} label The label detailing the action.
*/
_storeEvent: function(category, action, label) {
// ga might not be defined if donottrack is enabled, see index.html.
if (!window.ga) {
return;
}
// For now all we need to do is forward onto ga.
window.ga("send", "event", category, action, label);
},
/**
* Handles media permssion being obtained.
*/
gotMediaPermission: function() {
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
"Media granted");
},
/**
* Handles the user clicking the join room button.
*/
joinRoom: function() {
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
"Join the conversation");
},
/**
* Handles the user clicking the leave room button.
*/
leaveRoom: function() {
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
"Leave conversation");
},
/**
* Handles notification that two-way media has been achieved.
*/
mediaConnected: function() {
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
"Media connected")
},
/**
* Handles recording link clicks.
*
* @param {sharedActions.RecordClick} actionData The data associated with
* the link.
*/
recordClick: function(actionData) {
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.linkClick,
actionData.linkInfo);
},
/**
* Handles notifications that the activeRoomStore has changed, updating
* the metrics for room state and mute state as necessary.
*/
_onActiveRoomStoreChange: function() {
var roomStore = this.activeRoomStore.getStoreState();
this._checkRoomState(roomStore.roomState, roomStore.failureReason);
this._checkMuteState("audio", roomStore.audioMuted);
this._checkMuteState("video", roomStore.videoMuted);
},
/**
* Handles checking of the room state to look for events we need to send.
*
* @param {String} roomState The new room state.
* @param {String} failureReason Optional, if the room is in the failure
* state, this should contain the reason for
* the failure.
*/
_checkRoomState: function(roomState, failureReason) {
if (this._storeState.roomState === roomState) {
return;
}
this._storeState.roomState = roomState;
if (roomState === ROOM_STATES.FAILED &&
failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID) {
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
"Link expired or invalid");
}
if (roomState === ROOM_STATES.FULL) {
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
"Room full");
}
},
/**
* Handles check of the mute state to look for events we need to send.
*
* @param {String} type The type of mute being adjusted, i.e. "audio" or
* "video".
* @param {Boolean} muted The new state of mute
*/
_checkMuteState: function(type, muted) {
var muteItem = type + "Muted";
if (this._storeState[muteItem] === muted) {
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);
}
});
return StandaloneMetricsStore;
})();

View File

@ -160,11 +160,23 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
var StandaloneRoomHeader = React.createClass({displayName: "StandaloneRoomHeader",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
recordClick: function() {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: "Support link click"
}));
},
render: function() {
return (
React.createElement("header", null,
React.createElement("h1", null, mozL10n.get("clientShortname2")),
React.createElement("a", {target: "_blank", href: loop.config.generalSupportUrl},
React.createElement("a", {href: loop.config.generalSupportUrl,
onClick: this.recordClick,
target: "_blank"},
React.createElement("i", {className: "icon icon-help"})
)
)
@ -173,7 +185,14 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
var StandaloneRoomFooter = React.createClass({displayName: "StandaloneRoomFooter",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
_getContent: function() {
// We use this technique of static markup as it means we get
// just one overall string for L10n to define the structure of
// the whole item.
return mozL10n.get("legal_text_and_links", {
"clientShortname": mozL10n.get("clientShortname2"),
"terms_of_use_url": React.renderToStaticMarkup(
@ -189,10 +208,21 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
},
recordClick: function(event) {
// Check for valid href, as this is clicking on the paragraph -
// so the user may be clicking on the text rather than the link.
if (event.target && event.target.href) {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: event.target.href
}))
}
},
render: function() {
return (
React.createElement("footer", null,
React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()}}),
React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()},
onClick: this.recordClick}),
React.createElement("div", {className: "footer-logo"})
)
);
@ -201,10 +231,17 @@ loop.standaloneRoomViews = (function(mozL10n) {
var StandaloneRoomContextItem = React.createClass({displayName: "StandaloneRoomContextItem",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
receivingScreenShare: React.PropTypes.bool,
roomContextUrl: React.PropTypes.object
},
recordClick: function() {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: "Shared URL"
}));
},
render: function() {
if (!this.props.roomContextUrl ||
!this.props.roomContextUrl.location) {
@ -225,7 +262,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.createElement("img", {src: this.props.roomContextUrl.thumbnail}),
React.createElement("div", {className: "standalone-context-url-description-wrapper"},
this.props.roomContextUrl.description,
React.createElement("br", null), React.createElement("a", {href: location}, location)
React.createElement("br", null), React.createElement("a", {href: location,
onClick: this.recordClick,
target: "_blank"}, location)
)
)
);
@ -234,6 +273,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
var StandaloneRoomContextView = React.createClass({displayName: "StandaloneRoomContextView",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
receivingScreenShare: React.PropTypes.bool.isRequired,
roomContextUrls: React.PropTypes.array,
roomName: React.PropTypes.string,
@ -259,6 +299,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.createElement("div", {className: "standalone-room-info"},
React.createElement("h2", {className: "room-name"}, this.props.roomName),
React.createElement(StandaloneRoomContextItem, {
dispatcher: this.props.dispatcher,
receivingScreenShare: this.props.receivingScreenShare,
roomContextUrl: roomContextUrl})
)
@ -517,7 +558,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
return (
React.createElement("div", {className: "room-conversation-wrapper"},
React.createElement("div", {className: "beta-logo"}),
React.createElement(StandaloneRoomHeader, null),
React.createElement(StandaloneRoomHeader, {dispatcher: this.props.dispatcher}),
React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState,
failureReason: this.state.failureReason,
joinRoom: this.joinRoom,
@ -527,6 +568,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.createElement("div", {className: "video-layout-wrapper"},
React.createElement("div", {className: "conversation room-conversation"},
React.createElement(StandaloneRoomContextView, {
dispatcher: this.props.dispatcher,
receivingScreenShare: this.state.receivingScreenShare,
roomContextUrls: this.state.roomContextUrls,
roomName: this.state.roomName,
@ -556,7 +598,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.createElement(loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView, {
marketplaceSrc: this.state.marketplaceSrc,
onMarketplaceMessage: this.state.onMarketplaceMessage}),
React.createElement(StandaloneRoomFooter, null)
React.createElement(StandaloneRoomFooter, {dispatcher: this.props.dispatcher})
)
);
}
@ -564,6 +606,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
return {
StandaloneRoomContextView: StandaloneRoomContextView,
StandaloneRoomFooter: StandaloneRoomFooter,
StandaloneRoomHeader: StandaloneRoomHeader,
StandaloneRoomView: StandaloneRoomView
};
})(navigator.mozL10n);

View File

@ -160,11 +160,23 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
var StandaloneRoomHeader = React.createClass({
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
recordClick: function() {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: "Support link click"
}));
},
render: function() {
return (
<header>
<h1>{mozL10n.get("clientShortname2")}</h1>
<a target="_blank" href={loop.config.generalSupportUrl}>
<a href={loop.config.generalSupportUrl}
onClick={this.recordClick}
target="_blank">
<i className="icon icon-help"></i>
</a>
</header>
@ -173,7 +185,14 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
var StandaloneRoomFooter = React.createClass({
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
_getContent: function() {
// We use this technique of static markup as it means we get
// just one overall string for L10n to define the structure of
// the whole item.
return mozL10n.get("legal_text_and_links", {
"clientShortname": mozL10n.get("clientShortname2"),
"terms_of_use_url": React.renderToStaticMarkup(
@ -189,10 +208,21 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
},
recordClick: function(event) {
// Check for valid href, as this is clicking on the paragraph -
// so the user may be clicking on the text rather than the link.
if (event.target && event.target.href) {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: event.target.href
}))
}
},
render: function() {
return (
<footer>
<p dangerouslySetInnerHTML={{__html: this._getContent()}}></p>
<p dangerouslySetInnerHTML={{__html: this._getContent()}}
onClick={this.recordClick}></p>
<div className="footer-logo" />
</footer>
);
@ -201,10 +231,17 @@ loop.standaloneRoomViews = (function(mozL10n) {
var StandaloneRoomContextItem = React.createClass({
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
receivingScreenShare: React.PropTypes.bool,
roomContextUrl: React.PropTypes.object
},
recordClick: function() {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: "Shared URL"
}));
},
render: function() {
if (!this.props.roomContextUrl ||
!this.props.roomContextUrl.location) {
@ -225,7 +262,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
<img src={this.props.roomContextUrl.thumbnail} />
<div className="standalone-context-url-description-wrapper">
{this.props.roomContextUrl.description}
<br /><a href={location}>{location}</a>
<br /><a href={location}
onClick={this.recordClick}
target="_blank">{location}</a>
</div>
</div>
);
@ -234,6 +273,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
var StandaloneRoomContextView = React.createClass({
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
receivingScreenShare: React.PropTypes.bool.isRequired,
roomContextUrls: React.PropTypes.array,
roomName: React.PropTypes.string,
@ -259,6 +299,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
<div className="standalone-room-info">
<h2 className="room-name">{this.props.roomName}</h2>
<StandaloneRoomContextItem
dispatcher={this.props.dispatcher}
receivingScreenShare={this.props.receivingScreenShare}
roomContextUrl={roomContextUrl} />
</div>
@ -517,7 +558,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
return (
<div className="room-conversation-wrapper">
<div className="beta-logo" />
<StandaloneRoomHeader />
<StandaloneRoomHeader dispatcher={this.props.dispatcher} />
<StandaloneRoomInfoArea roomState={this.state.roomState}
failureReason={this.state.failureReason}
joinRoom={this.joinRoom}
@ -527,6 +568,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
<div className="video-layout-wrapper">
<div className="conversation room-conversation">
<StandaloneRoomContextView
dispatcher={this.props.dispatcher}
receivingScreenShare={this.state.receivingScreenShare}
roomContextUrls={this.state.roomContextUrls}
roomName={this.state.roomName}
@ -556,7 +598,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
<loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView
marketplaceSrc={this.state.marketplaceSrc}
onMarketplaceMessage={this.state.onMarketplaceMessage} />
<StandaloneRoomFooter />
<StandaloneRoomFooter dispatcher={this.props.dispatcher} />
</div>
);
}
@ -564,6 +606,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
return {
StandaloneRoomContextView: StandaloneRoomContextView,
StandaloneRoomFooter: StandaloneRoomFooter,
StandaloneRoomHeader: StandaloneRoomHeader,
StandaloneRoomView: StandaloneRoomView
};
})(navigator.mozL10n);

View File

@ -1087,8 +1087,16 @@ loop.webapp = (function($, _, OT, mozL10n) {
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: feedbackClient
});
var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
activeRoomStore: activeRoomStore
});
loop.store.StoreMixin.register({feedbackStore: feedbackStore});
loop.store.StoreMixin.register({
feedbackStore: feedbackStore,
// This isn't used in any views, but is saved here to ensure it
// is kept alive.
standaloneMetricsStore: standaloneMetricsStore
});
window.addEventListener("unload", function() {
dispatcher.dispatch(new sharedActions.WindowUnload());

View File

@ -1087,8 +1087,16 @@ loop.webapp = (function($, _, OT, mozL10n) {
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: feedbackClient
});
var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
activeRoomStore: activeRoomStore
});
loop.store.StoreMixin.register({feedbackStore: feedbackStore});
loop.store.StoreMixin.register({
feedbackStore: feedbackStore,
// This isn't used in any views, but is saved here to ensure it
// is kept alive.
standaloneMetricsStore: standaloneMetricsStore
});
window.addEventListener("unload", function() {
dispatcher.dispatch(new sharedActions.WindowUnload());

View File

@ -60,12 +60,14 @@
<script src="../../standalone/content/js/standaloneMozLoop.js"></script>
<script src="../../standalone/content/js/fxOSMarketplace.js"></script>
<script src="../../standalone/content/js/standaloneRoomViews.js"></script>
<script src="../../standalone/content/js/standaloneMetricsStore.js"></script>
<script src="../../standalone/content/js/webapp.js"></script>
<!-- Test scripts -->
<script src="standalone_client_test.js"></script>
<script src="standaloneAppStore_test.js"></script>
<script src="standaloneMozLoop_test.js"></script>
<script src="standaloneRoomViews_test.js"></script>
<script src="standaloneMetricsStore_test.js"></script>
<script src="webapp_test.js"></script>
<script src="multiplexGum_test.js"></script>
<script>

View File

@ -0,0 +1,142 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var expect = chai.expect;
describe("loop.store.StandaloneMetricsStore", function() {
"use strict";
var sandbox, dispatcher, store, fakeActiveRoomStore;
var sharedActions = loop.shared.actions;
var METRICS_GA_CATEGORY = loop.store.METRICS_GA_CATEGORY;
var METRICS_GA_ACTIONS = loop.store.METRICS_GA_ACTIONS;
var ROOM_STATES = loop.store.ROOM_STATES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
window.ga = sinon.stub();
var fakeStore = loop.store.createStore({
getInitialStoreState: function() {
return {
audioMuted: false,
roomState: ROOM_STATES.INIT,
videoMuted: false
};
}
});
fakeActiveRoomStore = new fakeStore(dispatcher);
store = new loop.store.StandaloneMetricsStore(dispatcher, {
activeRoomStore: fakeActiveRoomStore
});
});
afterEach(function() {
sandbox.restore();
delete window.ga;
});
describe("Action Handlers", function() {
beforeEach(function() {
});
it("should log an event on GotMediaPermission", function() {
store.gotMediaPermission();
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
"Media granted");
});
it("should log an event on JoinRoom", function() {
store.joinRoom();
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
"Join the conversation");
});
it("should log an event on LeaveRoom", function() {
store.leaveRoom();
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
"Leave conversation");
});
it("should log an event on MediaConnected", function() {
store.mediaConnected();
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
"Media connected");
});
it("should log an event on RecordClick", function() {
store.recordClick(new sharedActions.RecordClick({
linkInfo: "fake"
}));
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.linkClick,
"fake");
})
});
describe("Store Change Handlers", function() {
it("should log an event on room full", function() {
fakeActiveRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
"Room full");
});
it("should log an event when the room is expired or invalid", function() {
fakeActiveRoomStore.setStoreState({
roomState: ROOM_STATES.FAILED,
failureReason: FAILURE_DETAILS.EXPIRED_OR_INVALID
});
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
"Link expired or invalid");
});
it("should log an event when video mute is changed", function() {
fakeActiveRoomStore.setStoreState({
videoMuted: true
});
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.faceMute,
"mute");
});
it("should log an event when audio mute is changed", function() {
fakeActiveRoomStore.setStoreState({
audioMuted: true
});
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.audioMute,
"mute");
});
});
});

View File

@ -45,7 +45,10 @@ describe("loop.standaloneRoomViews", function() {
});
function mountTestComponent(extraProps) {
var props = _.extend({ receivingScreenShare: false }, extraProps);
var props = _.extend({
dispatcher: dispatcher,
receivingScreenShare: false
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(
loop.standaloneRoomViews.StandaloneRoomContextView, props));
@ -98,6 +101,48 @@ describe("loop.standaloneRoomViews", function() {
expect(view.getDOMNode().querySelector(".standalone-context-url")).eql(null);
});
it("should dispatch a RecordClick action when the link is clicked", function() {
var view = mountTestComponent({
roomName: "Mark's room",
roomContextUrls: [{
description: "Mark's super page",
location: "http://invalid.com",
thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
}]
});
TestUtils.Simulate.click(view.getDOMNode()
.querySelector(".standalone-context-url-description-wrapper > a"));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RecordClick({
linkInfo: "Shared URL"
}));
});
});
describe("StandaloneRoomHeader", function() {
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(
loop.standaloneRoomViews.StandaloneRoomHeader, {
dispatcher: dispatcher
}));
}
it("should dispatch a RecordClick action when the support link is clicked", function() {
var view = mountTestComponent();
TestUtils.Simulate.click(view.getDOMNode().querySelector("a"));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RecordClick({
linkInfo: "Support link click"
}));
});
});
describe("StandaloneRoomView", function() {