Browse Source

Merge branch 'master' of github.com:jcbrand/converse.js

JC Brand 12 years ago
parent
commit
6553b5ba73
8 changed files with 576 additions and 177 deletions
  1. 14 9
      CHANGES.rst
  2. 1 0
      Libraries/strophe.muc.js
  3. 45 3
      converse.css
  4. 201 49
      converse.js
  5. 43 0
      mock.js
  6. 239 0
      spec/ChatRoomSpec.js
  7. 14 102
      spec/MainSpec.js
  8. 19 14
      tests_main.js

+ 14 - 9
CHANGES.rst

@@ -4,22 +4,27 @@ Changelog
 0.3 (unreleased)
 ----------------
 
-- Add vCard support [jcbrand]
-- Remember custom status messages upon reload. [jcbrand] 
-- Remove jquery-ui dependency. [jcbrand]
+- Add vCard support 
+  [jcbrand]
+- Remember custom status messages upon reload. 
+  [jcbrand] 
+- Remove jquery-ui dependency. 
+  [jcbrand]
 - Use backbone.localStorage to store the contacts roster, open chatboxes and
-  chat messages. [jcbrand]
-- Fixed user status handling, which wasn't 100% according to the
-  spec. [jcbrand]
-- Separate messages according to day in chats. [jcbrand]
+  chat messages. 
+  [jcbrand]
+- Fixed user status handling, which wasn't 100% according to the spec. 
+  [jcbrand]
+- Separate messages according to day in chats. 
+  [jcbrand]
 - Add support for specifying the BOSH bind URL as configuration setting.
   [jcbrand]
 - Improve the message counter to only increment when the window is not focused 
   [witekdev]
 - Make fetching of list of chatrooms on a server a configuration option.
   [jcbrand]
-- Use service discovery to show whether a chatroom is password protected as
-  well as its number of occupents. [jcbrand]
+- Use service discovery to show all available features on a room.
+  [jcbrand]
 
 
 0.2 (2013-03-28)

+ 1 - 0
Libraries/strophe.muc.js

@@ -57,6 +57,7 @@
              *  (String) nick - Optional nickname to use in the chat room.
              *  (Function) msg_handler_cb - The function call to handle messages from the specified chat room.
              *  (Function) pres_handler_cb - The function call back to handle presence in the chat room.
+             *  (Function) roster_cb - The function call back to handle roster changes in the chat room.
              *  (String) password - The optional password to use. (password protected rooms only)
             */
             var msg, room_nick, _this = this;

+ 45 - 3
converse.css

@@ -92,7 +92,11 @@ img.spinner {
     display: block;
     font-size: 12px;
     padding: 0.5em 0 0 0.5em;
+    cursor: default;
+}
 
+ul.participant-list li.moderator {
+    color: #FE0007;
 }
 
 .chatroom form.sendXMPPMessage {
@@ -223,6 +227,7 @@ div.chat-title {
     text-overflow: ellipsis;
     white-space: nowrap;
     text-shadow: rgba(0,0,0,0.51) 0 -1px 0;
+    height: 1em;
 }
 
 .chat-head-chatbox,
@@ -441,7 +446,7 @@ form.search-xmpp-contact input {
     text-overflow: ellipsis;
     white-space: nowrap;
     display: inline-block;
-    width: 160px;
+    width: 170px;
 }
 
 #available-chatrooms dt,
@@ -468,6 +473,41 @@ dd.available-chatroom,
     text-shadow: 0 1px 0 rgba(250, 250, 250, 1);
 }
 
+.room-info {
+    font-size: 11px;
+    font-style: normal;
+    font-weight: normal;
+}
+
+p.room-info {
+    margin: 0;
+    padding: 0;
+    display: block;
+    white-space: normal;
+}
+
+a.room-info {
+    background: url('images/information.png') no-repeat right top;
+    width: 22px;
+    float: right;
+    display: none;
+}
+
+a.open-room {
+    display: inline-block;    
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow-x: hidden;
+}
+
+dd.available-chatroom:hover a.room-info {
+    display: inline-block;
+}
+
+dd.available-chatroom:hover a.open-room {
+    width: 75%;
+}
+
 #converse-roster dd a.remove-xmpp-contact {
     background: url('images/delete_icon.png') no-repeat right top;
     padding: 0 0 1em 0;
@@ -477,11 +517,11 @@ dd.available-chatroom,
     display: none;
 }
 
-#converse-roster  dd:hover *[class*="remove-xmpp-contact"] {
+#converse-roster  dd:hover a.remove-xmpp-contact {
     display: inline-block;
 }
 
-#converse-roster  dd:hover *[class*="open-chat"] {
+#converse-roster  dd:hover a.open-chat {
     width: 75%;
 }
 
@@ -541,10 +581,12 @@ form#converse-login {
 
 form#converse-login input {
     display: block;
+    width: 90%;
 }
 
 form#converse-login .login-submit {
     margin-top: 1em;
+    width: auto;
 }
 
 form.set-xmpp-status,

+ 201 - 49
converse.js

@@ -684,14 +684,56 @@
         events: {
             'submit form.add-chatroom': 'createChatRoom',
             'click input#show-rooms': 'showRooms',
-            'click a.open-room': 'createChatRoom'
+            'click a.open-room': 'createChatRoom',
+            'click a.room-info': 'showRoomInfo'
         },
         room_template: _.template(
-            '<dd class="available-chatroom">' +
-            '<a class="open-room {{classes}}" data-room-jid="{{jid}}"' +
-                ' title="{{desc}}"' +
-                ' href="#">' +
-            '{{name}}</a>&nbsp;{{occ}}</dd>'),
+            '<dd class="available-chatroom">'+
+            '<a class="open-room" data-room-jid="{{jid}}" title="Click to open this room" href="#">{{name}}</a>'+
+            '<a class="room-info" data-room-jid="{{jid}}" title="Show more information on this room" href="#">&nbsp;</a>'+
+            '</dd>'),
+
+        room_description_template: _.template(
+            '<div class="room-info">'+
+            '<p class="room-info"><strong>Description:</strong> {{desc}}</p>' +
+            '<p class="room-info"><strong>Occupants:</strong> {{occ}}</p>' +
+            '<p class="room-info"><strong>Features:</strong> <ul>'+
+            '{[ if (passwordprotected) { ]}' +
+                '<li class="room-info locked">Requires authentication</li>' +
+            '{[ } ]}' +
+            '{[ if (hidden) { ]}' +
+                '<li class="room-info">Hidden</li>' +
+            '{[ } ]}' +
+            '{[ if (membersonly) { ]}' +
+                '<li class="room-info">Requires an invitation</li>' +
+            '{[ } ]}' +
+            '{[ if (moderated) { ]}' +
+                '<li class="room-info">Moderated</li>' +
+            '{[ } ]}' +
+            '{[ if (nonanonymous) { ]}' +
+                '<li class="room-info">Non-anonymous</li>' +
+            '{[ } ]}' +
+            '{[ if (open) { ]}' +
+                '<li class="room-info">Open room</li>' +
+            '{[ } ]}' +
+            '{[ if (persistent) { ]}' +
+                '<li class="room-info">Permanent room</li>' +
+            '{[ } ]}' +
+            '{[ if (publicroom) { ]}' +
+                '<li class="room-info">Public</li>' +
+            '{[ } ]}' +
+            '{[ if (semianonymous) { ]}' +
+                '<li class="room-info">Semi-anonymous</li>' +
+            '{[ } ]}' +
+            '{[ if (temporary) { ]}' +
+                '<li class="room-info">Temporary room</li>' +
+            '{[ } ]}' +
+            '{[ if (unmoderated) { ]}' +
+                '<li class="room-info">Unmoderated</li>' +
+            '{[ } ]}' +
+            '</p>' +
+            '</div>'
+        ),
 
         tab_template: _.template('<li><a class="s" href="#chatrooms">Rooms</a></li>'),
 
@@ -720,37 +762,23 @@
             converse.connection.muc.listRooms(
                 this.muc_domain,
                 $.proxy(function (iq) { // Success
-                    var name, jid, i, that = this, $available_chatrooms = this.$el.find('#available-chatrooms');
-                    this.rdict = {};
+                    var name, jid, i, fragment,
+                        that = this,
+                        $available_chatrooms = this.$el.find('#available-chatrooms');
                     this.rooms = $(iq).find('query').find('item');
-                    this.rooms.each(function (i) { that.rdict[$(this).attr('jid')] = this; });
-                    this.fragment = document.createDocumentFragment();
                     if (this.rooms.length) {
                         $available_chatrooms.html('<dt>Rooms on '+this.muc_domain+'</dt>');
-                        _.each(this.rooms, $.proxy(function (room, idx) {
-                            converse.connection.disco.info(
-                                $(room).attr('jid'),
-                                null,
-                                $.proxy(function (stanza) {
-                                    var name = $(stanza).find('identity').attr('name');
-                                    var desc = $(stanza).find('field[var="muc#roominfo_description"] value').text();
-                                    var occ = $(stanza).find('field[var="muc#roominfo_occupants"] value').text();
-                                    var locked = $(stanza).find('feature[var="muc_passwordprotected"]').length;
-                                    var jid = $(stanza).attr('from');
-                                    var classes = locked && 'locked' || '';
-                                    delete this.rdict[jid];
-                                    this.$el.find('#available-chatrooms').append(
-                                        this.room_template({'name':name,
-                                                            'desc':desc,
-                                                            'occ':occ,
-                                                            'jid':jid,
-                                                            'classes': classes 
-                                        }));
-                                    if (_.keys(this.rdict).length === 0) {
-                                        $('input#show-rooms').show().siblings('img.spinner').remove();
-                                    }
-                                }, this));
-                        }, this));
+                        fragment = document.createDocumentFragment();
+                        for (i=0; i<this.rooms.length; i++) {
+                            name = Strophe.unescapeNode($(this.rooms[i]).attr('name')||$(this.rooms[i]).attr('jid'));
+                            jid = $(this.rooms[i]).attr('jid');
+                            fragment.appendChild($(this.room_template({
+                                'name':name,
+                                'jid':jid
+                                }))[0]);
+                        }
+                        $available_chatrooms.append(fragment);
+                        $('input#show-rooms').show().siblings('img.spinner').remove();
                     } else {
                         $available_chatrooms.html('<dt>No rooms on '+this.muc_domain+'</dt>');
                         $('input#show-rooms').show().siblings('img.spinner').remove();
@@ -780,6 +808,53 @@
             this.updateRoomsList();
         },
 
+        showRoomInfo: function (ev) {
+            var target = ev.target,
+                $dd = $(target).parent('dd'),
+                $div = $dd.find('div.room-info');
+            if ($div.length) {
+                $div.remove();
+            } else {
+                $dd.append('<img class="spinner" src="images/spinner.gif"/>');
+                converse.connection.disco.info(
+                    $(target).attr('data-room-jid'),
+                    null,
+                    $.proxy(function (stanza) {
+                        var $stanza = $(stanza);
+                        // All MUC features shown here: http://xmpp.org/registrar/disco-features.html
+                        var desc = $stanza.find('field[var="muc#roominfo_description"] value').text();
+                        var occ = $stanza.find('field[var="muc#roominfo_occupants"] value').text();
+                        var hidden = $stanza.find('feature[var="muc_hidden"]').length;
+                        var membersonly = $stanza.find('feature[var="muc_membersonly"]').length;
+                        var moderated = $stanza.find('feature[var="muc_moderated"]').length;
+                        var nonanonymous = $stanza.find('feature[var="muc_nonanonymous"]').length;
+                        var open = $stanza.find('feature[var="muc_open"]').length;
+                        var passwordprotected = $stanza.find('feature[var="muc_passwordprotected"]').length;
+                        var persistent = $stanza.find('feature[var="muc_persistent"]').length;
+                        var publicroom = $stanza.find('feature[var="muc_public"]').length;
+                        var semianonymous = $stanza.find('feature[var="muc_semianonymous"]').length;
+                        var temporary = $stanza.find('feature[var="muc_temporary"]').length;
+                        var unmoderated = $stanza.find('feature[var="muc_unmoderated"]').length;
+                        $dd.find('img.spinner').replaceWith(
+                            this.room_description_template({
+                                'desc':desc,
+                                'occ':occ,
+                                'hidden':hidden,
+                                'membersonly':membersonly,
+                                'moderated':moderated,
+                                'nonanonymous':nonanonymous,
+                                'open':open,
+                                'passwordprotected':passwordprotected,
+                                'persistent':persistent,
+                                'publicroom': publicroom,
+                                'semianonymous':semianonymous,
+                                'temporary':temporary,
+                                'unmoderated':unmoderated
+                            }));
+                    }, this));
+            }
+        },
+
         createChatRoom: function (ev) {
             ev.preventDefault();
             var name, server, jid, $name, $server, errors;
@@ -986,7 +1061,8 @@
                 this.model.get('nick'),
                 $.proxy(this.onChatRoomMessage, this),
                 $.proxy(this.onChatRoomPresence, this),
-                $.proxy(this.onChatRoomRoster, this));
+                $.proxy(this.onChatRoomRoster, this),
+                null);
 
             this.model.messages.on('add', this.showMessage, this);
             this.model.on('destroy', function (model, response, options) {
@@ -1004,22 +1080,84 @@
 
         onLeave: function () {},
 
+        showRoomConfigOptions: function (stanza) {
+            // FIXME: Show a proper configuration form
+            var $chat_content = this.$el.find('.chat-content'),
+                $stanza = $(stanza),
+                $fields = $stanza.find('field'),
+                title = $stanza.find('title').text(),
+                instructions = $stanza.find('instructions').text(),
+                i;
+            $chat_content.append(title);
+            $chat_content.append(instructions);
+            for (i=0; i<$fields.length; i++) {
+                $field = $($fields[i]);
+                $chat_content.append('<label>'+$field.attr('label')+'</label>');
+                // $chat_content.append('<input type="text" name=">'+$field.attr('label')+'</label>');
+            }
+        },
+
         onChatRoomPresence: function (presence, room) {
             var nick = room.nick,
                 $presence = $(presence),
-                from = $presence.attr('from');
+                from = $presence.attr('from'), item;
             if ($presence.attr('type') !== 'error') {
+                if ($presence.find("status[code='201']").length) {
+                    // This is a new chatroom. We create an instant
+                    // chatroom, and let the user manually set any
+                    // configuration setting. (2nd part is TODO)
+                    converse.connection.muc.createInstantRoom(room.name);
+                    /* TODO: Find a place for this code (it configures a
+                        * newly created chatroom).
+                        * -------------------------------------------------
+                    $item = $presence.find('item');
+                    if ($item.length) {
+                        if ($item.attr('affiliation') == 'owner') {
+                            if (false) {
+                            } else {
+                                converse.connection.muc.configure(
+                                    room.name, 
+                                    $.proxy(this.showRoomConfigOptions, this)
+                                );
+                            }
+                        }
+                    }
+                    */
+                }
                 // check for status 110 to see if it's our own presence
                 if ($presence.find("status[code='110']").length) {
-                    // check if server changed our nick
                     if ($presence.find("status[code='210']").length) {
+                        // check if server changed our nick
                         this.model.set({'nick': Strophe.getResourceFromJid(from)});
                     }
                 }
             } else {
-                var error = $presence.find('error');
-                if ($(error).attr('type') == 'auth') {
-                    this.$el.find('.chat-content').append('Sorry, this chatroom is restricted');
+                var $error = $presence.find('error'),
+                    $chat_content = this.$el.find('.chat-content');
+                if ($error.attr('type') == 'auth') {
+                    if ($error.find('not-authorized').length) {
+                        $chat_content.append('This chatroom requires a password');
+                    } else if ($error.find('registration-required').length) {
+                        $chat_content.append('You are not on the member list of this room');
+                    } else if ($error.find('forbidden').length) {
+                        $chat_content.append('You have been banned from this room');
+                    }
+                } else if ($error.attr('type') == 'modify') {
+                    if ($error.find('jid-malformed').length) {
+                        $chat_content.append('No nickname was specified');
+                    }
+                } else if ($error.attr('type') == 'cancel') {
+                    if ($error.find('not-allowed').length) {
+                        $chat_content.append('You are not allowed to create new rooms');
+                    } else if ($error.find('not-acceptable').length) {
+                        $chat_content.append("Your nickname doesn't conform to the room's policies");
+                    } else if ($error.find('conflict').length) {
+                        $chat_content.append("Your nickname is already taken");
+                    } else if ($error.find('item-not-found').length) {
+                        $chat_content.append("This room does not (yet) exist");
+                    } else if ($error.find('service-unavailable').length) {
+                        $chat_content.append("This room has reached it's maximum number of occupants");
+                    } 
                 }
             }
             return true;
@@ -1108,16 +1246,32 @@
             return true;
         },
 
+        occupant_template: _.template(
+            '<li class="{{role}}" '+
+                '{[ if (role === "moderator") { ]}' +
+                    'title="This user is a moderator"' +
+                '{[ } ]}'+
+                '{[ if (role === "participant") { ]}' +
+                    'title="This user can send messages in this room"' +
+                '{[ } ]}'+
+                '{[ if (role === "visitor") { ]}' +
+                    'title="This user can NOT send messages in this room"' +
+                '{[ } ]}'+
+            '>{{nick}}</li>'
+        ),
+
         onChatRoomRoster: function (roster, room) {
-            // underscore size is needed because roster is an object
             var controlboxview = converse.chatboxesview.views.controlbox,
                 roster_size = _.size(roster),
                 $participant_list = this.$el.find('.participant-list'),
-                participants = [],
-                i;
+                participants = [], keys = _.keys(roster), i;
             this.$el.find('.participant-list').empty();
             for (i=0; i<roster_size; i++) {
-                participants.push('<li>' + Strophe.unescapeNode(_.keys(roster)[i]) + '</li>');
+                participants.push(
+                    this.occupant_template({
+                        role: roster[keys[i]].role,
+                        nick: Strophe.unescapeNode(keys[i])
+                    }));
             }
             $participant_list.append(participants.join(""));
             return true;
@@ -1344,7 +1498,6 @@
                 this.$el.addClass('current-xmpp-contact');
                 this.$el.html(this.template(item.toJSON()));
             }
-
             return this;
         },
 
@@ -1802,7 +1955,6 @@
             converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message));
             this.save({'status_message': status_message});
         }
-
     });
 
     converse.XMPPStatusView = Backbone.View.extend({
@@ -1834,7 +1986,6 @@
                 '<a class="change-xmpp-status-message" href="#" Title="Click here to write a custom status message"></a>' +
             '</div>'),
 
-
         renderStatusChangeForm: function (ev) {
             ev.preventDefault();
             var status_message = this.model.get('status') || 'offline';
@@ -1937,6 +2088,7 @@
          * This collection stores Feature Models, representing features
          * provided by available XMPP entities (e.g. servers)
          * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
+         * All features are shown here: http://xmpp.org/registrar/disco-features.html
          */
         model: converse.Feature,
         initialize: function () {
@@ -2114,8 +2266,8 @@
 
     converse.onConnected = function (connection) {
         this.connection = connection;
-        // this.connection.xmlInput = function (body) { console.log(body); };
-        // this.connection.xmlOutput = function (body) { console.log(body); };
+        this.connection.xmlInput = function (body) { console.log(body); };
+        this.connection.xmlOutput = function (body) { console.log(body); };
         this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
         this.domain = Strophe.getDomainFromJid(this.connection.jid);
         this.features = new this.Features();

+ 43 - 0
mock.js

@@ -0,0 +1,43 @@
+(function (root, factory) {
+    define("mock", 
+        ['converse'], 
+        function() { 
+            return factory(); 
+        });
+}(this, function (converse) {
+    var mock_connection = {
+        'muc': {
+            'listRooms': function () {},
+            'join': function () {},
+            'leave': function () {}
+        },
+        'jid': 'dummy@localhost',
+        'addHandler': function (handler, ns, name, type, id, from, options) { 
+            return function () {};
+        },
+        'send': function () {},
+        'roster': {
+            'add': function () {},
+            'authorize': function () {},
+            'unauthorize': function () {},
+            'get': function () {},
+            'subscribe': function () {},
+            'registerCallback': function () {}
+        },
+        'vcard': { 
+            'get': function (callback, jid) {
+                var name = jid.split('@')[0].replace('.', ' ').split(' ');
+                var firstname = name[0].charAt(0).toUpperCase()+name[0].slice(1);
+                var lastname = name[1].charAt(0).toUpperCase()+name[1].slice(1);
+                var fullname = firstname+' '+lastname;
+                var vcard = $iq().c('vCard').c('FN').t(fullname);
+                callback(vcard.tree());
+            } 
+        },
+        'disco': {
+            'info': function () {},
+            'items': function () {}
+        }
+    };
+    return mock_connection;
+}));

+ 239 - 0
spec/ChatRoomSpec.js

@@ -0,0 +1,239 @@
+(function (root, factory) {
+    define([
+        "converse",
+        "mock"
+        ], function (converse, mock_connection) {
+            return factory(converse, mock_connection);
+        }
+    );
+} (this, function (converse, mock_connection) {
+    return describe("ChatRooms", $.proxy(function() {
+        var chatroom_names = [
+            'Dyon van de Wege', 'Thomas Kalb', 'Dirk Theissen', 'Felix Hofmann', 'Ka Lek', 'Anne Ebersbacher'
+        ];
+        describe("A Chat Room", $.proxy(function () {
+            beforeEach($.proxy(function () {
+                if (!$("div#controlbox").is(':visible')) {
+                    $('.toggle-online-users').click();
+                }
+                var roomspanel = this.chatboxesview.views.controlbox.roomspanel;
+                var $input = roomspanel.$el.find('input.new-chatroom-name');
+                var $server = roomspanel.$el.find('input.new-chatroom-server');
+                $input.val('lounge');
+                $server.val('muc.localhost');
+                roomspanel.$el.find('form').submit();
+                $('.toggle-online-users').click();
+            }, converse));
+
+            it("shows users currently present in the room", $.proxy(function () {
+                var chatroomview = this.chatboxesview.views['lounge@muc.localhost'];
+                var $participant_list = chatroomview.$el.find('.participant-list');
+                var roster = {}, room = {}, i;
+
+                for (i=0; i<chatroom_names.length-1; i++) {
+                    roster[chatroom_names[i]] = {};
+                    chatroomview.onChatRoomRoster(roster, room);
+                    expect($participant_list.find('li').length).toBe(1+i);
+                    expect($($participant_list.find('li')[i]).text()).toBe(chatroom_names[i]);
+                }
+                roster[converse.bare_jid] = {};
+                chatroomview.onChatRoomRoster(roster, room);
+            }, converse));
+
+            it("indicates moderators by means of a special css class and tooltip", $.proxy(function () {
+                var chatroomview = this.chatboxesview.views['lounge@muc.localhost'];
+                var $participant_list = chatroomview.$el.find('.participant-list');
+                var roster = {}, idx = chatroom_names.length-1;
+                roster[chatroom_names[idx]] = {};
+                roster[chatroom_names[idx]].role = 'moderator';
+                chatroomview.onChatRoomRoster(roster, {});
+                occupant = $participant_list.find('li');
+                expect(occupant.length).toBe(1);
+                expect($(occupant).text()).toBe(chatroom_names[idx]);
+                expect($(occupant).attr('class')).toBe('moderator');
+                expect($(occupant).attr('title')).toBe('This user is a moderator');
+            }, converse));
+
+            it("can be saved to, and retrieved from, localStorage", $.proxy(function () {
+                // We instantiate a new ChatBoxes collection, which by default
+                // will be empty.
+                var newchatboxes = new this.ChatBoxes();
+                expect(newchatboxes.length).toEqual(0);
+                // The chatboxes will then be fetched from localStorage inside the
+                // onConnected method
+                newchatboxes.onConnected();
+                expect(newchatboxes.length).toEqual(1);
+                // Check that the chatrooms retrieved from localStorage
+                // have the same attributes values as the original ones.
+                attrs = ['id', 'box_id', 'visible'];
+                for (i=0; i<attrs.length; i++) {
+                    new_attrs = _.pluck(_.pluck(newchatboxes.models, 'attributes'), attrs[i]);
+                    old_attrs = _.pluck(_.pluck(this.chatboxes.models, 'attributes'), attrs[i]);
+                    expect(_.isEqual(new_attrs, old_attrs)).toEqual(true);
+                }
+                this.rosterview.render();
+            }, converse));
+
+            it("can be closed again by clicking a DOM element with class 'close-chatbox-button'", $.proxy(function () {
+                var view = this.chatboxesview.views['lounge@muc.localhost'], chatroom = view.model, $el;
+                spyOn(view, 'closeChat').andCallThrough();
+                spyOn(converse.connection.muc, 'leave');
+                view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
+                view.$el.find('.close-chatbox-button').click();
+                expect(view.closeChat).toHaveBeenCalled();
+                expect(converse.connection.muc.leave).toHaveBeenCalled();
+            }, converse));
+        }, converse));
+
+        describe("When attempting to enter a chatroom", $.proxy(function () {
+            beforeEach($.proxy(function () {
+                var roomspanel = this.chatboxesview.views.controlbox.roomspanel;
+                var $input = roomspanel.$el.find('input.new-chatroom-name');
+                var $server = roomspanel.$el.find('input.new-chatroom-server');
+                $input.val('problematic');
+                $server.val('muc.localhost');
+                roomspanel.$el.find('form').submit();
+            }, converse));
+
+            afterEach($.proxy(function () {
+                var view = this.chatboxesview.views['problematic@muc.localhost'];
+                view.closeChat();
+            }, converse));
+
+            it("will show an error message if the room requires a password", $.proxy(function () {
+                var presence = $pres().attrs({
+                    from:'coven@chat.shakespeare.lit/thirdwitch',
+                    id:'n13mt3l',
+                    to:'hag66@shakespeare.lit/pda',
+                    type:'error'})
+                .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
+                .c('error').attrs({by:'coven@chat.shakespeare.lit', type:'auth'})
+                    .c('not-authorized').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
+                var view = this.chatboxesview.views['problematic@muc.localhost'];
+                view.onChatRoomPresence(presence, {'nick': 'dummy'});
+                var $chat_content = view.$el.find('.chat-content');
+                expect($chat_content.text()).toBe('This chatroom requires a password');
+            }, converse));
+
+            it("will show an error message if the room is members-only and the user not included", $.proxy(function () {
+                var presence = $pres().attrs({
+                    from:'coven@chat.shakespeare.lit/thirdwitch',
+                    id:'n13mt3l',
+                    to:'hag66@shakespeare.lit/pda',
+                    type:'error'})
+                .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
+                .c('error').attrs({by:'coven@chat.shakespeare.lit', type:'auth'})
+                    .c('registration-required').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
+                var view = this.chatboxesview.views['problematic@muc.localhost'];
+                view.onChatRoomPresence(presence, {'nick': 'dummy'});
+                var $chat_content = view.$el.find('.chat-content');
+                expect($chat_content.text()).toBe('You are not on the member list of this room');
+            }, converse));
+
+            it("will show an error message if the user has been banned", $.proxy(function () {
+                var presence = $pres().attrs({
+                    from:'coven@chat.shakespeare.lit/thirdwitch',
+                    id:'n13mt3l',
+                    to:'hag66@shakespeare.lit/pda',
+                    type:'error'})
+                .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
+                .c('error').attrs({by:'coven@chat.shakespeare.lit', type:'auth'})
+                    .c('forbidden').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
+                var view = this.chatboxesview.views['problematic@muc.localhost'];
+                view.onChatRoomPresence(presence, {'nick': 'dummy'});
+                var $chat_content = view.$el.find('.chat-content');
+                expect($chat_content.text()).toBe('You have been banned from this room');
+            }, converse));
+
+            it("will show an error message if no nickname was specified for the user", $.proxy(function () {
+                var presence = $pres().attrs({
+                    from:'coven@chat.shakespeare.lit/thirdwitch',
+                    id:'n13mt3l',
+                    to:'hag66@shakespeare.lit/pda',
+                    type:'error'})
+                .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
+                .c('error').attrs({by:'coven@chat.shakespeare.lit', type:'modify'})
+                    .c('jid-malformed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
+                var view = this.chatboxesview.views['problematic@muc.localhost'];
+                view.onChatRoomPresence(presence, {'nick': 'dummy'});
+                var $chat_content = view.$el.find('.chat-content');
+                expect($chat_content.text()).toBe('No nickname was specified');
+            }, converse));
+
+            it("will show an error message if the user is not allowed to have created the room", $.proxy(function () {
+                var presence = $pres().attrs({
+                    from:'coven@chat.shakespeare.lit/thirdwitch',
+                    id:'n13mt3l',
+                    to:'hag66@shakespeare.lit/pda',
+                    type:'error'})
+                .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
+                .c('error').attrs({by:'coven@chat.shakespeare.lit', type:'cancel'})
+                    .c('not-allowed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
+                var view = this.chatboxesview.views['problematic@muc.localhost'];
+                view.onChatRoomPresence(presence, {'nick': 'dummy'});
+                var $chat_content = view.$el.find('.chat-content');
+                expect($chat_content.text()).toBe('You are not allowed to create new rooms');
+            }, converse));
+
+            it("will show an error message if the user's nickname doesn't conform to room policy", $.proxy(function () {
+                var presence = $pres().attrs({
+                    from:'coven@chat.shakespeare.lit/thirdwitch',
+                    id:'n13mt3l',
+                    to:'hag66@shakespeare.lit/pda',
+                    type:'error'})
+                .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
+                .c('error').attrs({by:'coven@chat.shakespeare.lit', type:'cancel'})
+                    .c('not-acceptable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
+                var view = this.chatboxesview.views['problematic@muc.localhost'];
+                view.onChatRoomPresence(presence, {'nick': 'dummy'});
+                var $chat_content = view.$el.find('.chat-content');
+                expect($chat_content.text()).toBe("Your nickname doesn't conform to the room's policies");
+            }, converse));
+
+            it("will show an error message if the user's nickname is already taken", $.proxy(function () {
+                var presence = $pres().attrs({
+                    from:'coven@chat.shakespeare.lit/thirdwitch',
+                    id:'n13mt3l',
+                    to:'hag66@shakespeare.lit/pda',
+                    type:'error'})
+                .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
+                .c('error').attrs({by:'coven@chat.shakespeare.lit', type:'cancel'})
+                    .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
+                var view = this.chatboxesview.views['problematic@muc.localhost'];
+                view.onChatRoomPresence(presence, {'nick': 'dummy'});
+                var $chat_content = view.$el.find('.chat-content');
+                expect($chat_content.text()).toBe("Your nickname is already taken");
+            }, converse));
+
+            it("will show an error message if the room doesn't yet exist", $.proxy(function () {
+                var presence = $pres().attrs({
+                    from:'coven@chat.shakespeare.lit/thirdwitch',
+                    id:'n13mt3l',
+                    to:'hag66@shakespeare.lit/pda',
+                    type:'error'})
+                .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
+                .c('error').attrs({by:'coven@chat.shakespeare.lit', type:'cancel'})
+                    .c('item-not-found').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
+                var view = this.chatboxesview.views['problematic@muc.localhost'];
+                view.onChatRoomPresence(presence, {'nick': 'dummy'});
+                var $chat_content = view.$el.find('.chat-content');
+                expect($chat_content.text()).toBe("This room does not (yet) exist");
+            }, converse));
+
+            it("will show an error message if the room has reached it's maximum number of occupants", $.proxy(function () {
+                var presence = $pres().attrs({
+                    from:'coven@chat.shakespeare.lit/thirdwitch',
+                    id:'n13mt3l',
+                    to:'hag66@shakespeare.lit/pda',
+                    type:'error'})
+                .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up()
+                .c('error').attrs({by:'coven@chat.shakespeare.lit', type:'cancel'})
+                    .c('service-unavailable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
+                var view = this.chatboxesview.views['problematic@muc.localhost'];
+                view.onChatRoomPresence(presence, {'nick': 'dummy'});
+                var $chat_content = view.$el.find('.chat-content');
+                expect($chat_content.text()).toBe("This room has reached it's maximum number of occupants");
+            }, converse));
+        }, converse));
+    }, converse));
+}));

+ 14 - 102
spec/MainSpec.js

@@ -1,12 +1,12 @@
 (function (root, factory) {
     define([
-        "converse"
-        ], function (converse) {
-            return factory(converse);
+        "converse",
+        "mock"
+        ], function (converse, mock_connection) {
+            return factory(converse, mock_connection);
         }
     );
-} (this, function (converse) {
-
+} (this, function (converse, mock_connection) {
     return describe("Converse.js", $.proxy(function() {
         // Names from http://www.fakenamegenerator.com/
         var req_names = [
@@ -20,56 +20,7 @@
             'Robin Schook', 'Marcel Eberhardt', 'Simone Brauer', 'Asmaa Haakman', 'Felix Amsel',
             'Lena Grunewald', 'Laura Grunewald', 'Mandy Seiler', 'Sven Bosch', 'Nuriye Cuypers'
         ];
-        var chatroom_names = [
-            'Dyon van de Wege', 'Thomas Kalb', 'Dirk Theissen', 'Felix Hofmann', 'Ka Lek', 'Anne Ebersbacher'
-        ];
         var num_contacts = req_names.length + pend_names.length + cur_names.length;
-        mock_connection  = {
-            'muc': {
-                'listRooms': function () {},
-                'join': function () {},
-                'leave': function () {}
-            },
-            'jid': 'dummy@localhost',
-            'addHandler': function (handler, ns, name, type, id, from, options) { 
-                return function () {};
-            },
-            'send': function () {},
-            'roster': {
-                'add': function () {},
-                'authorize': function () {},
-                'unauthorize': function () {},
-                'get': function () {},
-                'subscribe': function () {},
-                'registerCallback': function () {}
-            },
-            'vcard': { 
-                'get': function (callback, jid) {
-                    var name = jid.split('@')[0].replace('.', ' ').split(' ');
-                    var firstname = name[0].charAt(0).toUpperCase()+name[0].slice(1);
-                    var lastname = name[1].charAt(0).toUpperCase()+name[1].slice(1);
-                    var fullname = firstname+' '+lastname;
-                    var vcard = $iq().c('vCard').c('FN').t(fullname);
-                    callback(vcard.tree());
-                } 
-            },
-            'disco': {
-                'info': function () {},
-                'items': function () {}
-            }
-        };
-
-        // Clear localStorage
-        window.localStorage.clear();
-        this.initialize({
-            prebind: false,
-            xhr_user_search: false,
-            auto_subscribe: false,
-            animate: false
-        });
-        this.onConnected(mock_connection);
-
-        // Variable declarations for specs
         var open_controlbox;
 
         describe("The Control Box", $.proxy(function () {
@@ -82,8 +33,10 @@
                 // open yet.
                 expect($("div#controlbox").is(':visible')).toBe(false);
                 spyOn(this, 'toggleControlBox').andCallThrough();
+                spyOn(this, 'showControlBox').andCallThrough();
                 $('.toggle-online-users').click();
                 expect(this.toggleControlBox).toHaveBeenCalled();
+                expect(this.showControlBox).toHaveBeenCalled();
                 expect($("div#controlbox").is(':visible')).toBe(true);
             }, converse);
             it("can be opened by clicking a DOM element with class 'toggle-online-users'", open_controlbox);
@@ -577,8 +530,13 @@
 
             it("is cleared when the window is focused", $.proxy(function () {
                 spyOn(converse, 'clearMsgCounter').andCallThrough();
-                $(window).trigger('focus');
-                expect(converse.clearMsgCounter).toHaveBeenCalled();
+                runs(function () {
+                    $(window).trigger('focus');
+                });
+                waits(50);
+                runs(function () {
+                    expect(converse.clearMsgCounter).toHaveBeenCalled();
+                });
             }, converse));
 
             it("is not incremented when the message is received and the window is focused", $.proxy(function () {
@@ -667,51 +625,5 @@
                 }, converse));
             }, converse));
         }, converse));
-
-        describe("A Chat Room", $.proxy(function () {
-            it("shows users currently present in the room", $.proxy(function () {
-                var chatroomview = this.chatboxesview.views['lounge@muc.localhost'];
-                var $participant_list = chatroomview.$el.find('.participant-list');
-                var roster = {}, room = {}, i;
-                for (i=0; i<chatroom_names.length; i++) {
-                    roster[chatroom_names[i]] = {};
-                    chatroomview.onChatRoomRoster(roster, room);
-                    expect($participant_list.find('li').length).toBe(1+i);
-                    expect($($participant_list.find('li')[i]).text()).toBe(chatroom_names[i]);
-                }
-                roster[converse.bare_jid] = {};
-                chatroomview.onChatRoomRoster(roster, room);
-            }, converse));
-
-            it("can be saved to, and retrieved from, localStorage", $.proxy(function () {
-                // We instantiate a new ChatBoxes collection, which by default
-                // will be empty.
-                var newchatboxes = new this.ChatBoxes();
-                expect(newchatboxes.length).toEqual(0);
-                // The chatboxes will then be fetched from localStorage inside the
-                // onConnected method
-                newchatboxes.onConnected();
-                expect(newchatboxes.length).toEqual(2); // controlbox is also included
-                // Check that the chatrooms retrieved from localStorage
-                // have the same attributes values as the original ones.
-                attrs = ['id', 'box_id', 'visible'];
-                for (i=0; i<attrs.length; i++) {
-                    new_attrs = _.pluck(_.pluck(newchatboxes.models, 'attributes'), attrs[i]);
-                    old_attrs = _.pluck(_.pluck(this.chatboxes.models, 'attributes'), attrs[i]);
-                    expect(_.isEqual(new_attrs, old_attrs)).toEqual(true);
-                }
-                this.rosterview.render();
-            }, converse));
-
-            it("can be closed again by clicking a DOM element with class 'close-chatbox-button'", $.proxy(function () {
-                var view = this.chatboxesview.views['lounge@muc.localhost'], chatroom = view.model, $el;
-                spyOn(view, 'closeChat').andCallThrough();
-                spyOn(converse.connection.muc, 'leave');
-                view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
-                view.$el.find('.close-chatbox-button').click();
-                expect(view.closeChat).toHaveBeenCalled();
-                expect(converse.connection.muc.leave).toHaveBeenCalled();
-            }, converse));
-        }, converse));
     }, converse));
 }));

+ 19 - 14
tests_main.js

@@ -1,16 +1,21 @@
-require(["jquery", "spec/MainSpec"], function($) {
-
-    $(function($) {
-        var jasmineEnv = jasmine.getEnv();
-        jasmineEnv.updateInterval = 500;
-
-        var htmlReporter = new jasmine.HtmlReporter();
-
-        jasmineEnv.addReporter(htmlReporter);
-
-        jasmineEnv.specFilter = function(spec) {
-            return htmlReporter.specFilter(spec);
-        };
-        jasmineEnv.execute();
+require(["jquery", "converse", "mock", "spec/MainSpec", "spec/ChatRoomSpec"], function($, converse, mock_connection) {
+    // Set up converse.js
+    window.localStorage.clear();
+    converse.initialize({
+        prebind: false,
+        xhr_user_search: false,
+        auto_subscribe: false,
+        animate: false
     });
+    converse.onConnected(mock_connection);
+
+    // Jasmine stuff
+    var jasmineEnv = jasmine.getEnv();
+    jasmineEnv.updateInterval = 50;
+    var htmlReporter = new jasmine.HtmlReporter();
+    jasmineEnv.addReporter(htmlReporter);
+    jasmineEnv.specFilter = function(spec) {
+        return htmlReporter.specFilter(spec);
+    };
+    jasmineEnv.execute();
 });