Commit bb468ae0 authored by JC Brand's avatar JC Brand

Add better support for XEP-0085. closes #292

Converse.js will now send chat state notifications of <paused>, <inactive> and
<gone> when the user has stopped typing for 30 seconds, 2 minutes and 10 minutes
respectively.
parent 3175bddc
...@@ -167,6 +167,7 @@ ...@@ -167,6 +167,7 @@
Strophe.error = function (msg) { console.log('ERROR: '+msg); }; Strophe.error = function (msg) { console.log('ERROR: '+msg); };
// Add Strophe Namespaces // Add Strophe Namespaces
Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
Strophe.addNamespace('REGISTER', 'jabber:iq:register'); Strophe.addNamespace('REGISTER', 'jabber:iq:register');
Strophe.addNamespace('XFORM', 'jabber:x:data'); Strophe.addNamespace('XFORM', 'jabber:x:data');
...@@ -187,7 +188,8 @@ ...@@ -187,7 +188,8 @@
var VERIFIED= 2; var VERIFIED= 2;
var FINISHED = 3; var FINISHED = 3;
var KEY = { var KEY = {
ENTER: 13 ENTER: 13,
FORWARD_SLASH: 47
}; };
var STATUS_WEIGHTS = { var STATUS_WEIGHTS = {
'offline': 6, 'offline': 6,
...@@ -197,11 +199,20 @@ ...@@ -197,11 +199,20 @@
'dnd': 2, 'dnd': 2,
'online': 1 'online': 1
}; };
// XEP-0085 Chat states
// http://xmpp.org/extensions/xep-0085.html
var INACTIVE = 'inactive'; var INACTIVE = 'inactive';
var ACTIVE = 'active'; var ACTIVE = 'active';
var COMPOSING = 'composing'; var COMPOSING = 'composing';
var PAUSED = 'paused'; var PAUSED = 'paused';
var GONE = 'gone'; var GONE = 'gone';
this.TIMEOUTS = { // Set as module attr so that we can override in tests.
'PAUSED': 30000,
'INACTIVE': 90000,
'GONE': 510000
};
var HAS_CSPRNG = ((typeof crypto !== 'undefined') && var HAS_CSPRNG = ((typeof crypto !== 'undefined') &&
((typeof crypto.randomBytes === 'function') || ((typeof crypto.randomBytes === 'function') ||
(typeof crypto.getRandomValues === 'function') (typeof crypto.getRandomValues === 'function')
...@@ -711,15 +722,16 @@ ...@@ -711,15 +722,16 @@
this.messages.browserStorage = new Backbone.BrowserStorage[converse.storage]( this.messages.browserStorage = new Backbone.BrowserStorage[converse.storage](
b64_sha1('converse.messages'+this.get('jid')+converse.bare_jid)); b64_sha1('converse.messages'+this.get('jid')+converse.bare_jid));
this.save({ this.save({
'chat_state': ACTIVE,
'box_id' : b64_sha1(this.get('jid')), 'box_id' : b64_sha1(this.get('jid')),
'height': height, 'height': height,
'minimized': this.get('minimized') || false, 'minimized': this.get('minimized') || false,
'num_unread': this.get('num_unread') || 0,
'otr_status': this.get('otr_status') || UNENCRYPTED, 'otr_status': this.get('otr_status') || UNENCRYPTED,
'time_minimized': this.get('time_minimized') || moment(), 'time_minimized': this.get('time_minimized') || moment(),
'time_opened': this.get('time_opened') || moment().valueOf(), 'time_opened': this.get('time_opened') || moment().valueOf(),
'user_id' : Strophe.getNodeFromJid(this.get('jid')), 'url': '',
'num_unread': this.get('num_unread') || 0, 'user_id' : Strophe.getNodeFromJid(this.get('jid'))
'url': ''
}); });
} else { } else {
this.set({ this.set({
...@@ -872,8 +884,8 @@ ...@@ -872,8 +884,8 @@
createMessage: function ($message) { createMessage: function ($message) {
var body = $message.children('body').text(), var body = $message.children('body').text(),
composing = $message.find('composing'), composing = $message.find(COMPOSING),
paused = $message.find('paused'), paused = $message.find(PAUSED),
delayed = $message.find('delay').length > 0, delayed = $message.find('delay').length > 0,
fullname = this.get('fullname'), fullname = this.get('fullname'),
is_groupchat = $message.attr('type') === 'groupchat', is_groupchat = $message.attr('type') === 'groupchat',
...@@ -976,10 +988,14 @@ ...@@ -976,10 +988,14 @@
this.model.messages.on('add', this.onMessageAdded, this); this.model.messages.on('add', this.onMessageAdded, this);
this.model.on('show', this.show, this); this.model.on('show', this.show, this);
this.model.on('destroy', this.hide, this); this.model.on('destroy', this.hide, this);
this.model.on('change', this.onChange, this); // TODO check for changed fullname as well
this.model.on('change:chat_state', this.sendChatState, this);
this.model.on('change:chat_status', this.onChatStatusChanged, this);
this.model.on('change:image', this.renderAvatar, this);
this.model.on('change:otr_status', this.onOTRStatusChanged, this);
this.model.on('change:minimized', this.onMinimizedChanged, this);
this.model.on('change:status', this.onStatusChanged, this);
this.model.on('showOTRError', this.showOTRError, this); this.model.on('showOTRError', this.showOTRError, this);
// XXX: doesn't look like this event is being used?
this.model.on('buddyStartsOTR', this.buddyStartsOTR, this);
this.model.on('showHelpMessages', this.showHelpMessages, this); this.model.on('showHelpMessages', this.showHelpMessages, this);
this.model.on('sendMessageStanza', this.sendMessageStanza, this); this.model.on('sendMessageStanza', this.sendMessageStanza, this);
this.model.on('showSentOTRMessage', function (text) { this.model.on('showSentOTRMessage', function (text) {
...@@ -1140,7 +1156,7 @@ ...@@ -1140,7 +1156,7 @@
var bare_jid = this.model.get('jid'); var bare_jid = this.model.get('jid');
var message = $msg({from: converse.connection.jid, to: bare_jid, type: 'chat', id: timestamp}) var message = $msg({from: converse.connection.jid, to: bare_jid, type: 'chat', id: timestamp})
.c('body').t(text).up() .c('body').t(text).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}); .c(ACTIVE, {'xmlns': Strophe.NS.CHATSTATES});
converse.connection.send(message); converse.connection.send(message);
if (converse.forward_messages) { if (converse.forward_messages) {
// Forward the message, so that other connected resources are also aware of it. // Forward the message, so that other connected resources are also aware of it.
...@@ -1190,25 +1206,40 @@ ...@@ -1190,25 +1206,40 @@
} }
}, },
keyPressed: function (ev) { sendChatState: function () {
var sendChatState = function () { /* XEP-0085 Chat State Notifications.
if (this.model.get('chat_state', 'composing')) { * Sends a message with the status of the user in this chat session
this.model.set('chat_state', 'paused'); * as taken from the 'chat_state' attribute of the chat box.
*/
converse.connection.send( converse.connection.send(
$msg({'to':this.model.get('jid'), 'type': 'chat'}) $msg({'to':this.model.get('jid'), 'type': 'chat'})
.c('paused', {'xmlns':'http://jabber.org/protocol/chatstates'}) .c(this.model.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES})
); );
// TODO: Set a new timeout here to send a chat_state of <gone> },
}
}; escalateChatState: function () {
var $textarea = $(ev.target), /* XEP-0085 Chat State Notifications.
args = arguments, * This method gets called asynchronously via setTimeout. It escalates the
context = this, * chat state and depending on the current state can set a new timeout.
message; */
var later = function() {
delete this.chat_state_timeout; delete this.chat_state_timeout;
sendChatState.apply(context, args); if (this.model.get('chat_state') == COMPOSING) {
}; this.model.set('chat_state', PAUSED);
// From now on, if no activity in 2 mins, we'll set the
// state to <inactive>
this.chat_state_timeout = setTimeout($.proxy(this.escalateChatState, this), converse.TIMEOUTS.INACTIVE);
} else if (this.model.get('chat_state') == PAUSED) {
this.model.set('chat_state', INACTIVE);
// From now on, if no activity in 10 mins, we'll set the
// state to <gone>
this.chat_state_timeout = setTimeout($.proxy(this.escalateChatState, this), converse.TIMEOUTS.GONE);
} else if (this.model.get('chat_state') == INACTIVE) {
this.model.set('chat_state', GONE);
}
},
keyPressed: function (ev) {
var $textarea = $(ev.target), message;
if (typeof this.chat_state_timeout !== 'undefined') { if (typeof this.chat_state_timeout !== 'undefined') {
clearTimeout(this.chat_state_timeout); clearTimeout(this.chat_state_timeout);
delete this.chat_state_timeout; // XXX: Necessary? delete this.chat_state_timeout; // XXX: Necessary?
...@@ -1225,21 +1256,15 @@ ...@@ -1225,21 +1256,15 @@
} }
converse.emit('messageSend', message); converse.emit('messageSend', message);
} }
this.model.set('chat_state', null); this.model.set('chat_state', ACTIVE);
// TODO: need to put timeout for <gone> state here?
} else if (!this.model.get('chatroom')) { } else if (!this.model.get('chatroom')) {
// chat state data is only for single user chat // chat state data is currently only for single user chat
this.chat_state_timeout = setTimeout(later, 10000); // Concerning group chat: http://xmpp.org/extensions/xep-0085.html#bizrules-groupchat
if (this.model.get('chat_state') != "composing") { this.chat_state_timeout = setTimeout($.proxy(this.escalateChatState, this), converse.TIMEOUTS.PAUSED);
if (ev.keyCode != 47) { if (this.model.get('chat_state') != COMPOSING && ev.keyCode != KEY.FORWARD_SLASH) {
// We don't send composing messages if the message // Set chat state to composing if keyCode is not a forward-slash
// starts with forward-slash. // (which would imply an internal command and not a message).
converse.connection.send( this.model.set('chat_state', COMPOSING);
$msg({'to':this.model.get('jid'), 'type': 'chat'})
.c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'})
);
}
this.model.set('chat_state', 'composing');
} }
} }
}, },
...@@ -1317,11 +1342,6 @@ ...@@ -1317,11 +1342,6 @@
console.log("OTR ERROR:"+msg); console.log("OTR ERROR:"+msg);
}, },
buddyStartsOTR: function (ev) {
this.showHelpMessages([__('This user has requested an encrypted session.')]);
this.model.initiateOTR();
},
startOTRFromToolbar: function (ev) { startOTRFromToolbar: function (ev) {
$(ev.target).parent().parent().slideUp(); $(ev.target).parent().parent().slideUp();
ev.stopPropagation(); ev.stopPropagation();
...@@ -1372,8 +1392,7 @@ ...@@ -1372,8 +1392,7 @@
}); });
}, },
onChange: function (item, changed) { onChatStatusChanged: function (item) {
if (_.has(item.changed, 'chat_status')) {
var chat_status = item.get('chat_status'), var chat_status = item.get('chat_status'),
fullname = item.get('fullname'); fullname = item.get('fullname');
fullname = _.isEmpty(fullname)? item.get('jid'): fullname; fullname = _.isEmpty(fullname)? item.get('jid'): fullname;
...@@ -1391,27 +1410,25 @@ ...@@ -1391,27 +1410,25 @@
converse.emit('contactStatusChanged', item.attributes, item.get('chat_status')); converse.emit('contactStatusChanged', item.attributes, item.get('chat_status'));
// TODO: DEPRECATED AND SHOULD BE REMOVED IN 0.9.0 // TODO: DEPRECATED AND SHOULD BE REMOVED IN 0.9.0
converse.emit('buddyStatusChanged', item.attributes, item.get('chat_status')); converse.emit('buddyStatusChanged', item.attributes, item.get('chat_status'));
} },
if (_.has(item.changed, 'status')) {
onStatusChanged: function (item) {
this.showStatusMessage(); this.showStatusMessage();
converse.emit('contactStatusMessageChanged', item.attributes, item.get('status')); converse.emit('contactStatusMessageChanged', item.attributes, item.get('status'));
// TODO: DEPRECATED AND SHOULD BE REMOVED IN 0.9.0 // TODO: DEPRECATED AND SHOULD BE REMOVED IN 0.9.0
converse.emit('buddyStatusMessageChanged', item.attributes, item.get('status')); converse.emit('buddyStatusMessageChanged', item.attributes, item.get('status'));
} },
if (_.has(item.changed, 'image')) {
this.renderAvatar(); onOTRStatusChanged: function (item) {
}
if (_.has(item.changed, 'otr_status')) {
this.renderToolbar().informOTRChange(); this.renderToolbar().informOTRChange();
} },
if (_.has(item.changed, 'minimized')) {
onMinimizedChanged: function (item) {
if (item.get('minimized')) { if (item.get('minimized')) {
this.hide(); this.hide();
} else { } else {
this.maximize(); this.maximize();
} }
}
// TODO check for changed fullname as well
}, },
showStatusMessage: function (msg) { showStatusMessage: function (msg) {
...@@ -3032,9 +3049,9 @@ ...@@ -3032,9 +3049,9 @@
}, },
showChat: function (attrs) { showChat: function (attrs) {
/* Find the chat box and show it. /* Find the chat box and show it. If it doesn't exist, create it.
* If it doesn't exist, create it.
*/ */
// TODO: Send the chat state ACTIVE to the contact once the chat box is opened.
var chatbox = this.model.get(attrs.jid); var chatbox = this.model.get(attrs.jid);
if (!chatbox) { if (!chatbox) {
chatbox = this.model.create(attrs, { chatbox = this.model.create(attrs, {
...@@ -3055,7 +3072,6 @@ ...@@ -3055,7 +3072,6 @@
this.MinimizedChatBoxView = Backbone.View.extend({ this.MinimizedChatBoxView = Backbone.View.extend({
tagName: 'div', tagName: 'div',
className: 'chat-head', className: 'chat-head',
events: { events: {
'click .close-chatbox-button': 'close', 'click .close-chatbox-button': 'close',
'click .restore-chat': 'restore' 'click .restore-chat': 'restore'
...@@ -3063,7 +3079,7 @@ ...@@ -3063,7 +3079,7 @@
initialize: function () { initialize: function () {
this.model.messages.on('add', function (m) { this.model.messages.on('add', function (m) {
if (!(m.get('composing') || m.get('paused'))) { if (!(m.get(COMPOSING) || m.get(PAUSED))) {
this.updateUnreadMessagesCounter(); this.updateUnreadMessagesCounter();
} }
}, this); }, this);
...@@ -3106,9 +3122,7 @@ ...@@ -3106,9 +3122,7 @@
}, },
restore: _.debounce(function (ev) { restore: _.debounce(function (ev) {
if (ev && ev.preventDefault) { if (ev && ev.preventDefault) { ev.preventDefault(); }
ev.preventDefault();
}
this.model.messages.off('add',null,this); this.model.messages.off('add',null,this);
this.remove(); this.remove();
this.model.maximize(); this.model.maximize();
...@@ -3117,7 +3131,6 @@ ...@@ -3117,7 +3131,6 @@
this.MinimizedChats = Backbone.Overview.extend({ this.MinimizedChats = Backbone.Overview.extend({
el: "#minimized-chats", el: "#minimized-chats",
events: { events: {
"click #toggle-minimized-chats": "toggle" "click #toggle-minimized-chats": "toggle"
}, },
...@@ -3348,17 +3361,7 @@ ...@@ -3348,17 +3361,7 @@
openChat: function (ev) { openChat: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); } if (ev && ev.preventDefault) { ev.preventDefault(); }
// XXX: Can this.model.attributes be used here, instead of return converse.chatboxviews.showChat(this.model.attributes);
// manually specifying all attributes?
return converse.chatboxviews.showChat({
'id': this.model.get('jid'),
'jid': this.model.get('jid'),
'fullname': this.model.get('fullname'),
'image_type': this.model.get('image_type'),
'image': this.model.get('image'),
'url': this.model.get('url'),
'status': this.model.get('status')
});
}, },
removeContact: function (ev) { removeContact: function (ev) {
...@@ -4457,7 +4460,7 @@ ...@@ -4457,7 +4460,7 @@
* TODO: these features need to be added in the relevant * TODO: these features need to be added in the relevant
* feature-providing Models, not here * feature-providing Models, not here
*/ */
converse.connection.disco.addFeature('http://jabber.org/protocol/chatstates'); // Limited support converse.connection.disco.addFeature(Strophe.NS.CHATSTATES);
converse.connection.disco.addFeature('http://jabber.org/protocol/rosterx'); // Limited support converse.connection.disco.addFeature('http://jabber.org/protocol/rosterx'); // Limited support
converse.connection.disco.addFeature('jabber:x:conference'); converse.connection.disco.addFeature('jabber:x:conference');
converse.connection.disco.addFeature('urn:xmpp:carbons:2'); converse.connection.disco.addFeature('urn:xmpp:carbons:2');
......
...@@ -7,6 +7,7 @@ Changelog ...@@ -7,6 +7,7 @@ Changelog
* Norwegian Bokmål translations. [Andreas Lorentsen] * Norwegian Bokmål translations. [Andreas Lorentsen]
* Updated Afrikaans translations. [jcbrand] * Updated Afrikaans translations. [jcbrand]
* Add responsiveness to CSS. We now use Sass preprocessor for generating CSS. [jcbrand] * Add responsiveness to CSS. We now use Sass preprocessor for generating CSS. [jcbrand]
* #292 Better support for XEP-0085 Chat State Notifications. [jcbrand]
0.8.6 (2014-12-07) 0.8.6 (2014-12-07)
------------------ ------------------
......
...@@ -408,11 +408,48 @@ ...@@ -408,11 +408,48 @@
runs(function () {}); runs(function () {});
}); });
it("can indicate a chat state notification", $.proxy(function () {
// See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
spyOn(converse, 'emit');
var sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@localhost';
// <composing> state
var msg = $msg({
from: sender_jid,
to: this.connection.jid,
type: 'chat',
id: (new Date()).getTime()
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
this.chatboxes.onMessage(msg);
expect(converse.emit).toHaveBeenCalledWith('message', msg);
var chatboxview = this.chatboxviews.get(sender_jid);
expect(chatboxview).toBeDefined();
// Check that the notification appears inside the chatbox in the DOM
var $events = chatboxview.$el.find('.chat-event');
expect($events.length).toBe(1);
expect($events.text()).toEqual(mock.cur_names[1].split(' ')[0] + ' is typing');
// <paused> state
msg = $msg({
from: sender_jid,
to: this.connection.jid,
type: 'chat',
id: (new Date()).getTime()
}).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
this.chatboxes.onMessage(msg);
expect(converse.emit).toHaveBeenCalledWith('message', msg);
$events = chatboxview.$el.find('.chat-event');
expect($events.length).toBe(1);
expect($events.text()).toEqual(mock.cur_names[1].split(' ')[0] + ' has stopped typing');
}, converse));
it("can be received which will open a chatbox and be displayed inside it", $.proxy(function () { it("can be received which will open a chatbox and be displayed inside it", $.proxy(function () {
spyOn(converse, 'emit'); spyOn(converse, 'emit');
var message = 'This is a received message'; var message = 'This is a received message';
var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
msg = $msg({ var msg = $msg({
from: sender_jid, from: sender_jid,
to: this.connection.jid, to: this.connection.jid,
type: 'chat', type: 'chat',
...@@ -691,6 +728,136 @@ ...@@ -691,6 +728,136 @@
}, converse)); }, converse));
}, converse)); }, converse));
describe("A Chat Status Notification", $.proxy(function () {
describe("A composing notifciation", $.proxy(function () {
it("is sent as soon as the user starts typing a message which is not a command", $.proxy(function () {
var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(contact_jid);
var view = this.chatboxviews.get(contact_jid);
expect(view.model.get('chat_state')).toBe('active');
spyOn(this.connection, 'send');
view.keyPressed({
target: view.$el.find('textarea.chat-textarea'),
keyCode: 1
});
expect(view.model.get('chat_state')).toBe('composing');
expect(this.connection.send).toHaveBeenCalled();
var $stanza = $(this.connection.send.argsForCall[0][0].tree());
expect($stanza.attr('to')).toBe(contact_jid);
expect($stanza.children().length).toBe(1);
expect($stanza.children().prop('tagName')).toBe('composing');
// The notification is not sent again
view.keyPressed({
target: view.$el.find('textarea.chat-textarea'),
keyCode: 1
});
expect(view.model.get('chat_state')).toBe('composing');
expect(converse.emit.callCount, 1);
}, converse));
}, converse));
describe("A paused notification", $.proxy(function () {
it("is sent if the user has stopped typing since 30 seconds", $.proxy(function () {
this.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test
var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(contact_jid);
var view = this.chatboxviews.get(contact_jid);
runs(function () {
expect(view.model.get('chat_state')).toBe('active');
view.keyPressed({
target: view.$el.find('textarea.chat-textarea'),
keyCode: 1
});
expect(view.model.get('chat_state')).toBe('composing');
spyOn(converse.connection, 'send');
});
waits(250);
runs(function () {
expect(view.model.get('chat_state')).toBe('paused');
expect(converse.connection.send).toHaveBeenCalled();
var $stanza = $(converse.connection.send.argsForCall[0][0].tree());
expect($stanza.attr('to')).toBe(contact_jid);
expect($stanza.children().length).toBe(1);
expect($stanza.children().prop('tagName')).toBe('paused');
});
}, converse));
}, converse));
describe("An inactive notifciation", $.proxy(function () {
it("is sent if the user has stopped typing since 2 minutes", $.proxy(function () {
// Make the timeouts shorter so that we can test
this.TIMEOUTS.PAUSED = 200;
this.TIMEOUTS.INACTIVE = 200;
var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(contact_jid);
var view = this.chatboxviews.get(contact_jid);
runs(function () {
expect(view.model.get('chat_state')).toBe('active');
view.keyPressed({
target: view.$el.find('textarea.chat-textarea'),
keyCode: 1
});
expect(view.model.get('chat_state')).toBe('composing');
});
waits(250);
runs(function () {
expect(view.model.get('chat_state')).toBe('paused');
spyOn(converse.connection, 'send');
});
waits(250);
runs(function () {
expect(view.model.get('chat_state')).toBe('inactive');
expect(converse.connection.send).toHaveBeenCalled();
var $stanza = $(converse.connection.send.argsForCall[0][0].tree());
expect($stanza.attr('to')).toBe(contact_jid);
expect($stanza.children().length).toBe(1);
expect($stanza.children().prop('tagName')).toBe('inactive');
});
}, converse));
}, converse));
describe("An gone notifciation", $.proxy(function () {
it("is sent if the user has stopped typing since 10 minutes", $.proxy(function () {
// Make the timeouts shorter so that we can test
this.TIMEOUTS.PAUSED = 200;
this.TIMEOUTS.INACTIVE = 200;
this.TIMEOUTS.GONE = 200;
var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(contact_jid);
var view = this.chatboxviews.get(contact_jid);
runs(function () {
expect(view.model.get('chat_state')).toBe('active');
view.keyPressed({
target: view.$el.find('textarea.chat-textarea'),
keyCode: 1
});
expect(view.model.get('chat_state')).toBe('composing');
});
waits(250);
runs(function () {
expect(view.model.get('chat_state')).toBe('paused');
});
waits(250);
runs(function () {
expect(view.model.get('chat_state')).toBe('inactive');
spyOn(converse.connection, 'send');
});
waits(250);
runs(function () {
expect(view.model.get('chat_state')).toBe('gone');
expect(converse.connection.send).toHaveBeenCalled();
var $stanza = $(converse.connection.send.argsForCall[0][0].tree());
expect($stanza.attr('to')).toBe(contact_jid);
expect($stanza.children().length).toBe(1);
expect($stanza.children().prop('tagName')).toBe('gone');
});
}, converse));
}, converse));
}, converse));
}, converse)); }, converse));
describe("Special Messages", $.proxy(function () { describe("Special Messages", $.proxy(function () {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment