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

This commit is contained in:
JC Brand 2013-05-11 16:26:31 +02:00
commit 6553b5ba73
8 changed files with 576 additions and 177 deletions

View File

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

View File

@ -57,6 +57,7 @@
* (String) nick - Optional nickname to use in the chat room. * (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) 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) 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) * (String) password - The optional password to use. (password protected rooms only)
*/ */
var msg, room_nick, _this = this; var msg, room_nick, _this = this;

View File

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

View File

@ -684,14 +684,56 @@
events: { events: {
'submit form.add-chatroom': 'createChatRoom', 'submit form.add-chatroom': 'createChatRoom',
'click input#show-rooms': 'showRooms', 'click input#show-rooms': 'showRooms',
'click a.open-room': 'createChatRoom' 'click a.open-room': 'createChatRoom',
'click a.room-info': 'showRoomInfo'
}, },
room_template: _.template( room_template: _.template(
'<dd class="available-chatroom">'+ '<dd class="available-chatroom">'+
'<a class="open-room {{classes}}" data-room-jid="{{jid}}"' + '<a class="open-room" data-room-jid="{{jid}}" title="Click to open this room" href="#">{{name}}</a>'+
' title="{{desc}}"' + '<a class="room-info" data-room-jid="{{jid}}" title="Show more information on this room" href="#">&nbsp;</a>'+
' href="#">' + '</dd>'),
'{{name}}</a>&nbsp;{{occ}}</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>'), tab_template: _.template('<li><a class="s" href="#chatrooms">Rooms</a></li>'),
@ -720,37 +762,23 @@
converse.connection.muc.listRooms( converse.connection.muc.listRooms(
this.muc_domain, this.muc_domain,
$.proxy(function (iq) { // Success $.proxy(function (iq) { // Success
var name, jid, i, that = this, $available_chatrooms = this.$el.find('#available-chatrooms'); var name, jid, i, fragment,
this.rdict = {}; that = this,
$available_chatrooms = this.$el.find('#available-chatrooms');
this.rooms = $(iq).find('query').find('item'); 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) { if (this.rooms.length) {
$available_chatrooms.html('<dt>Rooms on '+this.muc_domain+'</dt>'); $available_chatrooms.html('<dt>Rooms on '+this.muc_domain+'</dt>');
_.each(this.rooms, $.proxy(function (room, idx) { fragment = document.createDocumentFragment();
converse.connection.disco.info( for (i=0; i<this.rooms.length; i++) {
$(room).attr('jid'), name = Strophe.unescapeNode($(this.rooms[i]).attr('name')||$(this.rooms[i]).attr('jid'));
null, jid = $(this.rooms[i]).attr('jid');
$.proxy(function (stanza) { fragment.appendChild($(this.room_template({
var name = $(stanza).find('identity').attr('name'); 'name':name,
var desc = $(stanza).find('field[var="muc#roominfo_description"] value').text(); 'jid':jid
var occ = $(stanza).find('field[var="muc#roominfo_occupants"] value').text(); }))[0]);
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)); $available_chatrooms.append(fragment);
}, this)); $('input#show-rooms').show().siblings('img.spinner').remove();
} else { } else {
$available_chatrooms.html('<dt>No rooms on '+this.muc_domain+'</dt>'); $available_chatrooms.html('<dt>No rooms on '+this.muc_domain+'</dt>');
$('input#show-rooms').show().siblings('img.spinner').remove(); $('input#show-rooms').show().siblings('img.spinner').remove();
@ -780,6 +808,53 @@
this.updateRoomsList(); 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) { createChatRoom: function (ev) {
ev.preventDefault(); ev.preventDefault();
var name, server, jid, $name, $server, errors; var name, server, jid, $name, $server, errors;
@ -986,7 +1061,8 @@
this.model.get('nick'), this.model.get('nick'),
$.proxy(this.onChatRoomMessage, this), $.proxy(this.onChatRoomMessage, this),
$.proxy(this.onChatRoomPresence, this), $.proxy(this.onChatRoomPresence, this),
$.proxy(this.onChatRoomRoster, this)); $.proxy(this.onChatRoomRoster, this),
null);
this.model.messages.on('add', this.showMessage, this); this.model.messages.on('add', this.showMessage, this);
this.model.on('destroy', function (model, response, options) { this.model.on('destroy', function (model, response, options) {
@ -1004,22 +1080,84 @@
onLeave: function () {}, 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) { onChatRoomPresence: function (presence, room) {
var nick = room.nick, var nick = room.nick,
$presence = $(presence), $presence = $(presence),
from = $presence.attr('from'); from = $presence.attr('from'), item;
if ($presence.attr('type') !== 'error') { 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 // check for status 110 to see if it's our own presence
if ($presence.find("status[code='110']").length) { if ($presence.find("status[code='110']").length) {
// check if server changed our nick
if ($presence.find("status[code='210']").length) { if ($presence.find("status[code='210']").length) {
// check if server changed our nick
this.model.set({'nick': Strophe.getResourceFromJid(from)}); this.model.set({'nick': Strophe.getResourceFromJid(from)});
} }
} }
} else { } else {
var error = $presence.find('error'); var $error = $presence.find('error'),
if ($(error).attr('type') == 'auth') { $chat_content = this.$el.find('.chat-content');
this.$el.find('.chat-content').append('Sorry, this chatroom is restricted'); 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; return true;
@ -1108,16 +1246,32 @@
return true; 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) { onChatRoomRoster: function (roster, room) {
// underscore size is needed because roster is an object
var controlboxview = converse.chatboxesview.views.controlbox, var controlboxview = converse.chatboxesview.views.controlbox,
roster_size = _.size(roster), roster_size = _.size(roster),
$participant_list = this.$el.find('.participant-list'), $participant_list = this.$el.find('.participant-list'),
participants = [], participants = [], keys = _.keys(roster), i;
i;
this.$el.find('.participant-list').empty(); this.$el.find('.participant-list').empty();
for (i=0; i<roster_size; i++) { 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("")); $participant_list.append(participants.join(""));
return true; return true;
@ -1344,7 +1498,6 @@
this.$el.addClass('current-xmpp-contact'); this.$el.addClass('current-xmpp-contact');
this.$el.html(this.template(item.toJSON())); this.$el.html(this.template(item.toJSON()));
} }
return this; return this;
}, },
@ -1802,7 +1955,6 @@
converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message)); converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message));
this.save({'status_message': status_message}); this.save({'status_message': status_message});
} }
}); });
converse.XMPPStatusView = Backbone.View.extend({ 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>' + '<a class="change-xmpp-status-message" href="#" Title="Click here to write a custom status message"></a>' +
'</div>'), '</div>'),
renderStatusChangeForm: function (ev) { renderStatusChangeForm: function (ev) {
ev.preventDefault(); ev.preventDefault();
var status_message = this.model.get('status') || 'offline'; var status_message = this.model.get('status') || 'offline';
@ -1937,6 +2088,7 @@
* This collection stores Feature Models, representing features * This collection stores Feature Models, representing features
* provided by available XMPP entities (e.g. servers) * provided by available XMPP entities (e.g. servers)
* See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html * 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, model: converse.Feature,
initialize: function () { initialize: function () {
@ -2114,8 +2266,8 @@
converse.onConnected = function (connection) { converse.onConnected = function (connection) {
this.connection = connection; this.connection = connection;
// this.connection.xmlInput = function (body) { console.log(body); }; this.connection.xmlInput = function (body) { console.log(body); };
// this.connection.xmlOutput = function (body) { console.log(body); }; this.connection.xmlOutput = function (body) { console.log(body); };
this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid); this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
this.domain = Strophe.getDomainFromJid(this.connection.jid); this.domain = Strophe.getDomainFromJid(this.connection.jid);
this.features = new this.Features(); this.features = new this.Features();

43
mock.js Normal file
View File

@ -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
spec/ChatRoomSpec.js Normal file
View File

@ -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));
}));

View File

@ -1,12 +1,12 @@
(function (root, factory) { (function (root, factory) {
define([ define([
"converse" "converse",
], function (converse) { "mock"
return factory(converse); ], function (converse, mock_connection) {
return factory(converse, mock_connection);
} }
); );
} (this, function (converse) { } (this, function (converse, mock_connection) {
return describe("Converse.js", $.proxy(function() { return describe("Converse.js", $.proxy(function() {
// Names from http://www.fakenamegenerator.com/ // Names from http://www.fakenamegenerator.com/
var req_names = [ var req_names = [
@ -20,56 +20,7 @@
'Robin Schook', 'Marcel Eberhardt', 'Simone Brauer', 'Asmaa Haakman', 'Felix Amsel', 'Robin Schook', 'Marcel Eberhardt', 'Simone Brauer', 'Asmaa Haakman', 'Felix Amsel',
'Lena Grunewald', 'Laura Grunewald', 'Mandy Seiler', 'Sven Bosch', 'Nuriye Cuypers' '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; 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; var open_controlbox;
describe("The Control Box", $.proxy(function () { describe("The Control Box", $.proxy(function () {
@ -82,8 +33,10 @@
// open yet. // open yet.
expect($("div#controlbox").is(':visible')).toBe(false); expect($("div#controlbox").is(':visible')).toBe(false);
spyOn(this, 'toggleControlBox').andCallThrough(); spyOn(this, 'toggleControlBox').andCallThrough();
spyOn(this, 'showControlBox').andCallThrough();
$('.toggle-online-users').click(); $('.toggle-online-users').click();
expect(this.toggleControlBox).toHaveBeenCalled(); expect(this.toggleControlBox).toHaveBeenCalled();
expect(this.showControlBox).toHaveBeenCalled();
expect($("div#controlbox").is(':visible')).toBe(true); expect($("div#controlbox").is(':visible')).toBe(true);
}, converse); }, converse);
it("can be opened by clicking a DOM element with class 'toggle-online-users'", open_controlbox); 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 () { it("is cleared when the window is focused", $.proxy(function () {
spyOn(converse, 'clearMsgCounter').andCallThrough(); spyOn(converse, 'clearMsgCounter').andCallThrough();
runs(function () {
$(window).trigger('focus'); $(window).trigger('focus');
});
waits(50);
runs(function () {
expect(converse.clearMsgCounter).toHaveBeenCalled(); expect(converse.clearMsgCounter).toHaveBeenCalled();
});
}, converse)); }, converse));
it("is not incremented when the message is received and the window is focused", $.proxy(function () { it("is not incremented when the message is received and the window is focused", $.proxy(function () {
@ -667,51 +625,5 @@
}, converse)); }, converse));
}, converse)); }, 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)); }, converse));
})); }));

View File

@ -1,16 +1,21 @@
require(["jquery", "spec/MainSpec"], function($) { 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);
$(function($) { // Jasmine stuff
var jasmineEnv = jasmine.getEnv(); var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 500; jasmineEnv.updateInterval = 50;
var htmlReporter = new jasmine.HtmlReporter(); var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter); jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = function(spec) { jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec); return htmlReporter.specFilter(spec);
}; };
jasmineEnv.execute(); jasmineEnv.execute();
}); });
});