/* 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/. */ /*global loop, sinon, React */ /* jshint newcap:false */ var expect = chai.expect; var l10n = document.webL10n || document.mozL10n; var TestUtils = React.addons.TestUtils; describe("loop.shared.views", function() { "use strict"; var sharedModels = loop.shared.models, sharedViews = loop.shared.views, getReactElementByClass = TestUtils.findRenderedDOMComponentWithClass, sandbox; beforeEach(function() { sandbox = sinon.sandbox.create(); sandbox.useFakeTimers(); // exposes sandbox.clock as a fake timer }); afterEach(function() { $("#fixtures").empty(); sandbox.restore(); }); describe("L10nView", function() { beforeEach(function() { sandbox.stub(l10n, "translate"); }); it("should translate generated contents on render()", function() { var TestView = loop.shared.views.L10nView.extend(); var view = new TestView(); view.render(); sinon.assert.calledOnce(l10n.translate); sinon.assert.calledWithExactly(l10n.translate, view.el); }); }); describe("MediaControlButton", function() { it("should render an enabled local audio button", function() { var comp = TestUtils.renderIntoDocument(sharedViews.MediaControlButton({ scope: "local", type: "audio", action: function(){}, enabled: true })); expect(comp.getDOMNode().classList.contains("muted")).eql(false); }); it("should render a muted local audio button", function() { var comp = TestUtils.renderIntoDocument(sharedViews.MediaControlButton({ scope: "local", type: "audio", action: function(){}, enabled: false })); expect(comp.getDOMNode().classList.contains("muted")).eql(true); }); it("should render an enabled local video button", function() { var comp = TestUtils.renderIntoDocument(sharedViews.MediaControlButton({ scope: "local", type: "video", action: function(){}, enabled: true })); expect(comp.getDOMNode().classList.contains("muted")).eql(false); }); it("should render a muted local video button", function() { var comp = TestUtils.renderIntoDocument(sharedViews.MediaControlButton({ scope: "local", type: "video", action: function(){}, enabled: false })); expect(comp.getDOMNode().classList.contains("muted")).eql(true); }); }); describe("ConversationToolbar", function() { var hangup, publishStream; function mountTestComponent(props) { return TestUtils.renderIntoDocument( sharedViews.ConversationToolbar(props)); } beforeEach(function() { hangup = sandbox.stub(); publishStream = sandbox.stub(); }); it("should hangup when hangup button is clicked", function() { var comp = mountTestComponent({ hangup: hangup, publishStream: publishStream, audio: {enabled: true} }); TestUtils.Simulate.click( comp.getDOMNode().querySelector(".btn-hangup")); sinon.assert.calledOnce(hangup); sinon.assert.calledWithExactly(hangup); }); it("should unpublish audio when audio mute btn is clicked", function() { var comp = mountTestComponent({ hangup: hangup, publishStream: publishStream, audio: {enabled: true} }); TestUtils.Simulate.click( comp.getDOMNode().querySelector(".btn-mute-audio")); sinon.assert.calledOnce(publishStream); sinon.assert.calledWithExactly(publishStream, "audio", false); }); it("should publish audio when audio mute btn is clicked", function() { var comp = mountTestComponent({ hangup: hangup, publishStream: publishStream, audio: {enabled: false} }); TestUtils.Simulate.click( comp.getDOMNode().querySelector(".btn-mute-audio")); sinon.assert.calledOnce(publishStream); sinon.assert.calledWithExactly(publishStream, "audio", true); }); it("should unpublish video when video mute btn is clicked", function() { var comp = mountTestComponent({ hangup: hangup, publishStream: publishStream, video: {enabled: true} }); TestUtils.Simulate.click( comp.getDOMNode().querySelector(".btn-mute-video")); sinon.assert.calledOnce(publishStream); sinon.assert.calledWithExactly(publishStream, "video", false); }); it("should publish video when video mute btn is clicked", function() { var comp = mountTestComponent({ hangup: hangup, publishStream: publishStream, video: {enabled: false} }); TestUtils.Simulate.click( comp.getDOMNode().querySelector(".btn-mute-video")); sinon.assert.calledOnce(publishStream); sinon.assert.calledWithExactly(publishStream, "video", true); }); }); describe("ConversationView", function() { var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model; function mountTestComponent(props) { return TestUtils.renderIntoDocument(sharedViews.ConversationView(props)); } beforeEach(function() { fakeSessionData = { sessionId: "sessionId", sessionToken: "sessionToken", apiKey: "apiKey" }; fakeSession = _.extend({ connection: {connectionId: 42}, connect: sandbox.spy(), disconnect: sandbox.spy(), publish: sandbox.spy(), unpublish: sandbox.spy(), subscribe: sandbox.spy() }, Backbone.Events); fakePublisher = _.extend({ publishAudio: sandbox.spy(), publishVideo: sandbox.spy() }, Backbone.Events); fakeSDK = { initPublisher: sandbox.stub().returns(fakePublisher), initSession: sandbox.stub().returns(fakeSession) }; model = new sharedModels.ConversationModel(fakeSessionData, { sdk: fakeSDK, pendingCallTimeout: 1000 }); }); describe("#componentDidMount", function() { it("should start a session", function() { sandbox.stub(model, "startSession"); mountTestComponent({sdk: fakeSDK, model: model}); sinon.assert.calledOnce(model.startSession); }); }); describe("constructed", function() { var comp; beforeEach(function() { comp = mountTestComponent({sdk: fakeSDK, model: model}); }); describe("#hangup", function() { beforeEach(function() { comp.startPublishing(); }); it("should disconnect the session", function() { sandbox.stub(model, "endSession"); comp.hangup(); sinon.assert.calledOnce(model.endSession); }); it("should stop publishing local streams", function() { comp.hangup(); sinon.assert.calledOnce(fakeSession.unpublish); }); }); describe("#startPublishing", function() { beforeEach(function() { sandbox.stub(fakePublisher, "on"); }); it("should publish local stream", function() { comp.startPublishing(); sinon.assert.calledOnce(fakeSDK.initPublisher); sinon.assert.calledOnce(fakeSession.publish); }); it("should start listening to OT publisher accessDialogOpened and " + " accessDenied events", function() { comp.startPublishing(); sinon.assert.called(fakePublisher.on); sinon.assert.calledWith(fakePublisher.on, "accessDialogOpened accessDenied"); }); }); describe("#stopPublishing", function() { beforeEach(function() { sandbox.stub(fakePublisher, "off"); comp.startPublishing(); }); it("should stop publish local stream", function() { comp.stopPublishing(); sinon.assert.calledOnce(fakeSession.unpublish); }); it("should unsubscribe from publisher events", function() { comp.stopPublishing(); // Note: Backbone.Events#stopListening calls off() on passed object. sinon.assert.calledOnce(fakePublisher.off); }); }); describe("#publishStream", function() { var comp; beforeEach(function() { comp = mountTestComponent({sdk: fakeSDK, model: model}); comp.startPublishing(); }); it("should start streaming local audio", function() { comp.publishStream("audio", true); sinon.assert.calledOnce(fakePublisher.publishAudio); sinon.assert.calledWithExactly(fakePublisher.publishAudio, true); }); it("should stop streaming local audio", function() { comp.publishStream("audio", false); sinon.assert.calledOnce(fakePublisher.publishAudio); sinon.assert.calledWithExactly(fakePublisher.publishAudio, false); }); it("should start streaming local video", function() { comp.publishStream("video", true); sinon.assert.calledOnce(fakePublisher.publishVideo); sinon.assert.calledWithExactly(fakePublisher.publishVideo, true); }); it("should stop streaming local video", function() { comp.publishStream("video", false); sinon.assert.calledOnce(fakePublisher.publishVideo); sinon.assert.calledWithExactly(fakePublisher.publishVideo, false); }); }); describe("Model events", function() { it("should start streaming on session:connected", function() { model.trigger("session:connected"); sinon.assert.calledOnce(fakeSDK.initPublisher); }); it("should publish remote streams on session:stream-created", function() { var s1 = {connection: {connectionId: 42}}; var s2 = {connection: {connectionId: 43}}; model.trigger("session:stream-created", {streams: [s1, s2]}); sinon.assert.calledOnce(fakeSession.subscribe); sinon.assert.calledWith(fakeSession.subscribe, s2); }); it("should unpublish local stream on session:ended", function() { comp.startPublishing(); model.trigger("session:ended"); sinon.assert.calledOnce(fakeSession.unpublish); }); it("should unpublish local stream on session:peer-hungup", function() { comp.startPublishing(); model.trigger("session:peer-hungup"); sinon.assert.calledOnce(fakeSession.unpublish); }); it("should unpublish local stream on session:network-disconnected", function() { comp.startPublishing(); model.trigger("session:network-disconnected"); sinon.assert.calledOnce(fakeSession.unpublish); }); }); describe("Publisher events", function() { beforeEach(function() { comp.startPublishing(); }); it("should set audio state on streamCreated", function() { fakePublisher.trigger("streamCreated", {stream: {hasAudio: true}}); expect(comp.state.audio.enabled).eql(true); fakePublisher.trigger("streamCreated", {stream: {hasAudio: false}}); expect(comp.state.audio.enabled).eql(false); }); it("should set video state on streamCreated", function() { fakePublisher.trigger("streamCreated", {stream: {hasVideo: true}}); expect(comp.state.video.enabled).eql(true); fakePublisher.trigger("streamCreated", {stream: {hasVideo: false}}); expect(comp.state.video.enabled).eql(false); }); it("should set media state on streamDestroyed", function() { fakePublisher.trigger("streamDestroyed"); expect(comp.state.audio.enabled).eql(false); expect(comp.state.video.enabled).eql(false); }); }); }); }); describe("FeedbackView", function() { var comp, fakeFeedbackApiClient; beforeEach(function() { sandbox.stub(l10n, "get", function(x) { return x; }); fakeFeedbackApiClient = {send: sandbox.stub()}; comp = TestUtils.renderIntoDocument(sharedViews.FeedbackView({ feedbackApiClient: fakeFeedbackApiClient })); }); // local test helpers function clickHappyFace(comp) { var happyFace = comp.getDOMNode().querySelector(".face-happy"); TestUtils.Simulate.click(happyFace); } function clickSadFace(comp) { var sadFace = comp.getDOMNode().querySelector(".face-sad"); TestUtils.Simulate.click(sadFace); } function fillSadFeedbackForm(comp, category, text) { TestUtils.Simulate.change( comp.getDOMNode().querySelector("[value='" + category + "']")); if (text) { TestUtils.Simulate.change( comp.getDOMNode().querySelector("[name='description']"), { target: {value: "fake reason"} }); } } function submitSadFeedbackForm(comp, category, text) { TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form")); } describe("Happy feedback", function() { it("should send feedback data when clicking on the happy face", function() { clickHappyFace(comp); sinon.assert.calledOnce(fakeFeedbackApiClient.send); sinon.assert.calledWith(fakeFeedbackApiClient.send, {happy: true}); }); it("should thank the user once happy feedback data is sent", function() { fakeFeedbackApiClient.send = function(data, cb) { cb(); }; clickHappyFace(comp); expect(comp.getDOMNode() .querySelectorAll(".feedback .thank-you").length).eql(1); expect(comp.getDOMNode().querySelector("button.back")).to.be.a("null"); }); }); describe("Sad feedback", function() { it("should bring the user to feedback form when clicking on the sad face", function() { clickSadFace(comp); expect(comp.getDOMNode().querySelectorAll("form").length).eql(1); }); it("should disable the form submit button when no category is chosen", function() { clickSadFace(comp); expect(comp.getDOMNode() .querySelector("form button").disabled).eql(true); }); it("should disable the form submit button when the 'other' category is " + "chosen but no description has been entered yet", function() { clickSadFace(comp); fillSadFeedbackForm(comp, "other"); expect(comp.getDOMNode() .querySelector("form button").disabled).eql(true); }); it("should enable the form submit button when the 'other' category is " + "chosen and a description is entered", function() { clickSadFace(comp); fillSadFeedbackForm(comp, "other", "fake"); expect(comp.getDOMNode() .querySelector("form button").disabled).eql(false); }); it("should empty the description field when a predefined category is " + "chosen", function() { clickSadFace(comp); fillSadFeedbackForm(comp, "confusing"); expect(comp.getDOMNode() .querySelector("form input[type='text']").value).eql(""); }); it("should enable the form submit button once a predefined category is " + "chosen", function() { clickSadFace(comp); fillSadFeedbackForm(comp, "confusing"); expect(comp.getDOMNode() .querySelector("form button").disabled).eql(false); }); it("should disable the form submit button once the form is submitted", function() { clickSadFace(comp); fillSadFeedbackForm(comp, "confusing"); submitSadFeedbackForm(comp); expect(comp.getDOMNode() .querySelector("form button").disabled).eql(true); }); it("should send feedback data when the form is submitted", function() { clickSadFace(comp); fillSadFeedbackForm(comp, "confusing"); submitSadFeedbackForm(comp); sinon.assert.calledOnce(fakeFeedbackApiClient.send); sinon.assert.calledWithMatch(fakeFeedbackApiClient.send, { happy: false, category: "confusing" }); }); it("should send feedback data when user has entered a custom description", function() { clickSadFace(comp); fillSadFeedbackForm(comp, "other", "fake reason"); submitSadFeedbackForm(comp); sinon.assert.calledOnce(fakeFeedbackApiClient.send); sinon.assert.calledWith(fakeFeedbackApiClient.send, { happy: false, category: "other", description: "fake reason" }); }); it("should thank the user when feedback data has been sent", function() { fakeFeedbackApiClient.send = function(data, cb) { cb(); }; clickSadFace(comp); fillSadFeedbackForm(comp, "confusing"); submitSadFeedbackForm(comp); expect(comp.getDOMNode() .querySelectorAll(".feedback .thank-you").length).eql(1); }); }); }); describe("NotificationView", function() { var collection, model, view; beforeEach(function() { $("#fixtures").append('
'); model = new sharedModels.NotificationModel({ level: "error", message: "plop" }); collection = new sharedModels.NotificationCollection([model]); view = new sharedViews.NotificationView({ el: $("#test-notif"), collection: collection, model: model }); }); describe("#dismiss", function() { it("should automatically dismiss notification after 500ms", function() { view.render().dismiss({preventDefault: sandbox.spy()}); expect(view.$(".message").text()).eql("plop"); sandbox.clock.tick(500); expect(collection).to.have.length.of(0); expect($("#test-notif").html()).eql(undefined); }); }); describe("#render", function() { it("should render template with model attribute values", function() { view.render(); expect(view.$(".message").text()).eql("plop"); }); }); }); describe("NotificationListView", function() { var coll, notifData, testNotif; beforeEach(function() { sandbox.stub(l10n, "get", function(x) { return "translated:" + x; }); notifData = {level: "error", message: "plop"}; testNotif = new sharedModels.NotificationModel(notifData); coll = new sharedModels.NotificationCollection(); }); describe("#initialize", function() { it("should accept a collection option", function() { var view = new sharedViews.NotificationListView({collection: coll}); expect(view.collection).to.be.an.instanceOf( sharedModels.NotificationCollection); }); it("should set a default collection when none is passed", function() { var view = new sharedViews.NotificationListView(); expect(view.collection).to.be.an.instanceOf( sharedModels.NotificationCollection); }); }); describe("#clear", function() { it("should clear all notifications from the collection", function() { var view = new sharedViews.NotificationListView(); view.notify(testNotif); view.clear(); expect(coll).to.have.length.of(0); }); }); describe("#notify", function() { var view; beforeEach(function() { view = new sharedViews.NotificationListView({collection: coll}); }); describe("adds a new notification to the stack", function() { it("using a plain object", function() { view.notify(notifData); expect(coll).to.have.length.of(1); }); it("using a NotificationModel instance", function() { view.notify(testNotif); expect(coll).to.have.length.of(1); }); }); }); describe("#notifyL10n", function() { var view; beforeEach(function() { view = new sharedViews.NotificationListView({collection: coll}); }); it("should translate a message string identifier", function() { view.notifyL10n("fakeId", "warning"); sinon.assert.calledOnce(l10n.get); sinon.assert.calledWithExactly(l10n.get, "fakeId"); }); it("should notify end user with the provided message", function() { sandbox.stub(view, "notify"); view.notifyL10n("fakeId", "warning"); sinon.assert.calledOnce(view.notify); sinon.assert.calledWithExactly(view.notify, { message: "translated:fakeId", level: "warning" }); }); }); describe("#warn", function() { it("should add a warning notification to the stack", function() { var view = new sharedViews.NotificationListView({collection: coll}); view.warn("watch out"); expect(coll).to.have.length.of(1); expect(coll.at(0).get("level")).eql("warning"); expect(coll.at(0).get("message")).eql("watch out"); }); }); describe("#warnL10n", function() { it("should warn using a l10n string id", function() { var view = new sharedViews.NotificationListView({collection: coll}); sandbox.stub(view, "notify"); view.warnL10n("fakeId"); sinon.assert.called(view.notify); sinon.assert.calledWithExactly(view.notify, { message: "translated:fakeId", level: "warning" }); }); }); describe("#error", function() { it("should add an error notification to the stack", function() { var view = new sharedViews.NotificationListView({collection: coll}); view.error("wrong"); expect(coll).to.have.length.of(1); expect(coll.at(0).get("level")).eql("error"); expect(coll.at(0).get("message")).eql("wrong"); }); }); describe("#errorL10n", function() { it("should notify an error using a l10n string id", function() { var view = new sharedViews.NotificationListView({collection: coll}); sandbox.stub(view, "notify"); view.errorL10n("fakeId"); sinon.assert.called(view.notify); sinon.assert.calledWithExactly(view.notify, { message: "translated:fakeId", level: "error" }); }); }); describe("Collection events", function() { var view; beforeEach(function() { sandbox.stub(sharedViews.NotificationListView.prototype, "render"); view = new sharedViews.NotificationListView({collection: coll}); }); it("should render when a notification is added to the collection", function() { coll.add(testNotif); sinon.assert.calledOnce(view.render); }); it("should render when a notification is removed from the collection", function() { coll.add(testNotif); coll.remove(testNotif); sinon.assert.calledTwice(view.render); }); it("should render when the collection is reset", function() { coll.reset(); sinon.assert.calledOnce(view.render); }); }); }); });