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)
----------------
- 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)

View File

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

View File

@ -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,

View File

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

View File

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