Browse Source

Alphabetically sort roster contacts according to type and status

Also added a new jasmine spec for this as well as jquery.tinysort to do the
sorting.
JC Brand 12 years ago
parent
commit
39c0823f2a
4 changed files with 494 additions and 71 deletions
  1. 211 0
      Libraries/jquery.tinysort.js
  2. 73 70
      converse.js
  3. 208 0
      spec/RosterSpec.js
  4. 2 1
      tests_main.js

+ 211 - 0
Libraries/jquery.tinysort.js

@@ -0,0 +1,211 @@
+/*! TinySort 1.4.29
+* Copyright (c) 2008-2012 Ron Valstar http://www.sjeiti.com/
+*
+* Dual licensed under the MIT and GPL licenses:
+*   http://www.opensource.org/licenses/mit-license.php
+*   http://www.gnu.org/licenses/gpl.html
+*//*
+* Description:
+*   A jQuery plugin to sort child nodes by (sub) contents or attributes.
+*
+* Contributors:
+*	brian.gibson@gmail.com
+*	michael.thornberry@gmail.com
+*
+* Usage:
+*   $("ul#people>li").tsort();
+*   $("ul#people>li").tsort("span.surname");
+*   $("ul#people>li").tsort("span.surname",{order:"desc"});
+*   $("ul#people>li").tsort({place:"end"});
+*
+* Change default like so:
+*   $.tinysort.defaults.order = "desc";
+*
+* in this update:
+* 	- added plugin hook
+*   - stripped non-latin character ordering and turned it into a plugin
+*
+* in last update:
+*   - header comment no longer stripped in minified version
+*	- revision number no longer corresponds to svn revision since it's now git
+*
+* Todos:
+* 	- todo: uppercase vs lowercase
+* 	- todo: 'foobar' != 'foobars' in non-latin
+*
+*/
+;(function($) {
+	// private vars
+	var fls = !1							// minify placeholder
+		,nll = null							// minify placeholder
+		,prsflt = parseFloat				// minify placeholder
+		,mathmn = Math.min					// minify placeholder
+		,rxLastNr = /(-?\d+\.?\d*)$/g		// regex for testing strings ending on numbers
+		,aPluginPrepare = []
+		,aPluginSort = []
+	;
+	//
+	// init plugin
+	$.tinysort = {
+		 id: 'TinySort'
+		,version: '1.4.29'
+		,copyright: 'Copyright (c) 2008-2012 Ron Valstar'
+		,uri: 'http://tinysort.sjeiti.com/'
+		,licensed: {
+			MIT: 'http://www.opensource.org/licenses/mit-license.php'
+			,GPL: 'http://www.gnu.org/licenses/gpl.html'
+		}
+		,plugin: function(prepare,sort){
+			aPluginPrepare.push(prepare);	// function(settings){doStuff();}
+			aPluginSort.push(sort);			// function(valuesAreNumeric,sA,sB,iReturn){doStuff();return iReturn;}
+		}
+		,defaults: { // default settings
+
+			 order: 'asc'			// order: asc, desc or rand
+
+			,attr: nll				// order by attribute value
+			,data: nll				// use the data attribute for sorting
+			,useVal: fls			// use element value instead of text
+
+			,place: 'start'			// place ordered elements at position: start, end, org (original position), first
+			,returns: fls			// return all elements or only the sorted ones (true/false)
+
+			,cases: fls				// a case sensitive sort orders [aB,aa,ab,bb]
+			,forceStrings:fls		// if false the string '2' will sort with the value 2, not the string '2'
+
+			,sortFunction: nll		// override the default sort function
+		}
+	};
+	$.fn.extend({
+		tinysort: function(_find,_settings) {
+			if (_find&&typeof(_find)!='string') {
+				_settings = _find;
+				_find = nll;
+			}
+
+			var oSettings = $.extend({}, $.tinysort.defaults, _settings)
+				,sParent
+				,oThis = this
+				,iLen = $(this).length
+				,oElements = {} // contains sortable- and non-sortable list per parent
+				,bFind = !(!_find||_find=='')
+				,bAttr = !(oSettings.attr===nll||oSettings.attr=="")
+				,bData = oSettings.data!==nll
+				// since jQuery's filter within each works on array index and not actual index we have to create the filter in advance
+				,bFilter = bFind&&_find[0]==':'
+				,$Filter = bFilter?oThis.filter(_find):oThis
+				,fnSort = oSettings.sortFunction
+				,iAsc = oSettings.order=='asc'?1:-1
+				,aNewOrder = []
+			;
+
+			$.each(aPluginPrepare,function(i,fn){
+				fn.call(fn,oSettings);
+			});
+
+
+			if (!fnSort) fnSort = oSettings.order=='rand'?function() {
+				return Math.random()<.5?1:-1;
+			}:function(a,b) {
+				var bNumeric = fls
+				// maybe toLower
+					,sA = !oSettings.cases?toLowerCase(a.s):a.s
+					,sB = !oSettings.cases?toLowerCase(b.s):b.s;
+				// maybe force Strings
+//				var bAString = typeof(sA)=='string';
+//				var bBString = typeof(sB)=='string';
+//				if (!oSettings.forceStrings&&(bAString||bBString)) {
+//					if (!bAString) sA = ''+sA;
+//					if (!bBString) sB = ''+sB;
+				if (!oSettings.forceStrings) {
+					// maybe mixed
+					var  aAnum = sA&&sA.match(rxLastNr)
+						,aBnum = sB&&sB.match(rxLastNr);
+					if (aAnum&&aBnum) {
+						var  sAprv = sA.substr(0,sA.length-aAnum[0].length)
+							,sBprv = sB.substr(0,sB.length-aBnum[0].length);
+						if (sAprv==sBprv) {
+							bNumeric = !fls;
+							sA = prsflt(aAnum[0]);
+							sB = prsflt(aBnum[0]);
+						}
+					}
+				}
+				// return sort-integer
+				var iReturn = iAsc*(sA<sB?-1:(sA>sB?1:0));
+
+				$.each(aPluginSort,function(i,fn){
+					iReturn = fn.call(fn,bNumeric,sA,sB,iReturn);
+				});
+
+				return iReturn;
+			};
+
+			oThis.each(function(i,el) {
+				var $Elm = $(el)
+					// element or sub selection
+					,mElmOrSub = bFind?(bFilter?$Filter.filter(el):$Elm.find(_find)):$Elm
+					// text or attribute value
+					,sSort = bData?''+mElmOrSub.data(oSettings.data):(bAttr?mElmOrSub.attr(oSettings.attr):(oSettings.useVal?mElmOrSub.val():mElmOrSub.text()))
+ 					// to sort or not to sort
+					,mParent = $Elm.parent();
+				if (!oElements[mParent])	oElements[mParent] = {s:[],n:[]};	// s: sort, n: not sort
+				if (mElmOrSub.length>0)		oElements[mParent].s.push({s:sSort,e:$Elm,n:i}); // s:string, e:element, n:number
+				else						oElements[mParent].n.push({e:$Elm,n:i});
+			});
+			//
+			// sort
+			for (sParent in oElements) oElements[sParent].s.sort(fnSort);
+			//
+			// order elements and fill new order
+			for (sParent in oElements) {
+				var oParent = oElements[sParent]
+					,aOrg = [] // list for original position
+					,iLow = iLen
+					,aCnt = [0,0] // count how much we've sorted for retreival from either the sort list or the non-sort list (oParent.s/oParent.n)
+					,i;
+				switch (oSettings.place) {
+					case 'first':	$.each(oParent.s,function(i,obj) { iLow = mathmn(iLow,obj.n) }); break;
+					case 'org':		$.each(oParent.s,function(i,obj) { aOrg.push(obj.n) }); break;
+					case 'end':		iLow = oParent.n.length; break;
+					default:		iLow = 0;
+				}
+				for (i = 0;i<iLen;i++) {
+					var bSList = contains(aOrg,i)?!fls:i>=iLow&&i<iLow+oParent.s.length
+						,mEl = (bSList?oParent.s:oParent.n)[aCnt[bSList?0:1]].e;
+					mEl.parent().append(mEl);
+					if (bSList||!oSettings.returns) aNewOrder.push(mEl.get(0));
+					aCnt[bSList?0:1]++;
+				}
+			}
+			oThis.length = 0;
+			Array.prototype.push.apply(oThis,aNewOrder);
+			return oThis;
+		}
+	});
+	// toLowerCase
+	function toLowerCase(s) {
+		return s&&s.toLowerCase?s.toLowerCase():s;
+	}
+	// array contains
+	function contains(a,n) {
+		for (var i=0,l=a.length;i<l;i++) if (a[i]==n) return !fls;
+		return fls;
+	}
+	// set functions
+	$.fn.TinySort = $.fn.Tinysort = $.fn.tsort = $.fn.tinysort;
+})(jQuery);
+
+/*! Array.prototype.indexOf for IE (issue #26) */
+if (!Array.prototype.indexOf) {
+	Array.prototype.indexOf = function(elt /*, from*/) {
+		var len = this.length
+			,from = Number(arguments[1])||0;
+		from = from<0?Math.ceil(from):Math.floor(from);
+		if (from<0) from += len;
+		for (;from<len;from++){
+			if (from in this && this[from]===elt) return from;
+		}
+		return -1;
+	};
+}

+ 73 - 70
converse.js

@@ -43,12 +43,13 @@
         define([
             "Libraries/burry.js/burry",
             "Libraries/underscore.string",
+            "Libraries/jquery.tinysort",
             "Libraries/jquery-ui-1.9.1.custom",
             "Libraries/sjcl",
             "Libraries/backbone",
             "Libraries/strophe.muc",
             "Libraries/strophe.roster"
-            ], function (Burry, _s, logging) {
+            ], function (Burry, _s) {
                 var store = new Burry.Store('collective.xmpp.chat');
                 // Init underscore.str
                 _.str = _s;
@@ -1246,7 +1247,7 @@
                 that = this,
                 subscription = item.get('subscription');
 
-            $(this.el).addClass(item.get('presence_type')).attr('id', 'online-users-'+item.get('user_id'));
+            $(this.el).addClass(item.get('presence_type'));
             
             if (ask === 'subscribe') {
                 this.$el.addClass('pending-xmpp-contact');
@@ -1521,78 +1522,80 @@
         }
     });
 
-    xmppchat.RosterView= (function (roster, _, $, console) {
-        var View = Backbone.View.extend({
-            el: $('#xmppchat-roster'),
-            model: roster,
-            rosteritemviews: {},
-
-            initialize: function () {
-                this.model.on("add", function (item) {
-                    var view = new xmppchat.RosterItemView({model: item});
-                    this.rosteritemviews[item.id] = view;
-                    if (item.get('ask') === 'request') {
-                        view.on('decline-request', function (item) {
-                            this.model.remove(item.id);
-                        }, this);
-                    }
-                    this.render();
-                }, this);
-
-                this.model.on('change', function (item) {
-                    this.render();
-                }, this);
-
-                this.model.on("remove", function (item) {
-                    delete this.rosteritemviews[item.id];
-                    this.render();
-                }, this);
-            },
-
-            template: _.template('<dt id="xmpp-contact-requests">Contact requests</dt>' +
-                                '<dt id="xmpp-contacts">My contacts</dt>' +
-                                '<dt id="pending-xmpp-contacts">Pending contacts</dt>'),
-
-            render: function () {
-                this.$el.empty().html(this.template());
-                var models = this.model.sort().models,
-                    children = $(this.el).children(),
-                    my_contacts = this.$el.find('#xmpp-contacts').hide(),
-                    contact_requests = this.$el.find('#xmpp-contact-requests').hide(),
-                    pending_contacts = this.$el.find('#pending-xmpp-contacts').hide(),
-                    $count, num;
-
-                for (var i=0; i<models.length; i++) {
-                    var model = models[i],
-                        user_id = Strophe.getNodeFromJid(model.id),
-                        view = this.rosteritemviews[model.id],
-                        ask = model.get('ask'),
-                        subscription = model.get('subscription');
-
-                    if (ask === 'subscribe') {
-                        pending_contacts.after(view.render().el);
-                    } else if (ask === 'request') {
-                        contact_requests.after(view.render().el);
-                    } else if (subscription === 'both') {
-                        my_contacts.after(view.render().el);
-                    } 
+    xmppchat.RosterView = Backbone.View.extend({
+        el: $('#xmppchat-roster'),
+        rosteritemviews: {},
+
+        initialize: function () {
+            this.model.on("add", function (item) {
+                var view = new xmppchat.RosterItemView({model: item});
+                this.rosteritemviews[item.id] = view;
+                if (item.get('ask') === 'request') {
+                    view.on('decline-request', function (item) {
+                        this.model.remove(item.id);
+                    }, this);
                 }
-                // Hide the headings if there are no contacts under them
-                _.each([my_contacts, contact_requests, pending_contacts], function (h) {
-                    if (h.nextUntil('dt').length > 0) {
-                        h.show();
-                    }
-                });
-                $count = $('#online-count');
-                $count.text(this.model.getNumOnlineContacts());
+                this.render();
+            }, this);
+
+            this.model.on('change', function (item) {
+                this.render();
+            }, this);
+
+            this.model.on("remove", function (item) {
+                delete this.rosteritemviews[item.id];
+                this.render();
+            }, this);
+        },
+
+        template: _.template('<dt id="xmpp-contact-requests">Contact requests</dt>' +
+                            '<dt id="xmpp-contacts">My contacts</dt>' +
+                            '<dt id="pending-xmpp-contacts">Pending contacts</dt>'),
+
+        render: function () {
+            this.$el.empty().html(this.template());
+            var models = this.model.sort().models,
+                children = $(this.el).children(),
+                $my_contacts = this.$el.find('#xmpp-contacts').hide(),
+                $contact_requests = this.$el.find('#xmpp-contact-requests').hide(),
+                $pending_contacts = this.$el.find('#pending-xmpp-contacts').hide(),
+                $count, num;
+
+            for (var i=0; i<models.length; i++) {
+                var model = models[i],
+                    user_id = Strophe.getNodeFromJid(model.id),
+                    view = this.rosteritemviews[model.id],
+                    ask = model.get('ask'),
+                    subscription = model.get('subscription');
+                    crit = {order:'asc'};
+
+                if (ask === 'subscribe') {
+                    $pending_contacts.after(view.render().el);
+                    $pending_contacts.after($pending_contacts.siblings('dd.pending-xmpp-contact').tsort(crit));
+                } else if (ask === 'request') {
+                    $contact_requests.after(view.render().el);
+                    $contact_requests.after($contact_requests.siblings('dd.requesting-xmpp-contact').tsort(crit));
+                } else if (subscription === 'both') {
+                    $my_contacts.after(view.render().el);
+                    $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
+                    $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
+                    $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.away').tsort('a', crit));
+                    $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.busy').tsort('a', crit));
+                    $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.online').tsort('a', crit));
+                } 
             }
-        });
-        var view = new View();
-        return view;
+            // Hide the headings if there are no contacts under them
+            _.each([$my_contacts, $contact_requests, $pending_contacts], function (h) {
+                if (h.nextUntil('dt').length > 0) {
+                    h.show();
+                }
+            });
+            $count = $('#online-count');
+            $count.text(this.model.getNumOnlineContacts());
+        }
     });
 
     xmppchat.XMPPStatus = Backbone.Model.extend({
-
         initialize: function () {
             this.set({
                 'status' : this.getStatus(),
@@ -1781,7 +1784,7 @@
             this.chatboxesview = new this.ChatBoxesView({'model': this.chatboxes});
 
             this.roster = new this.RosterItems();
-            this.rosterview = Backbone.View.extend(this.RosterView(this.roster, _, $, console));
+            this.rosterview = new this.RosterView({'model':this.roster});
 
             this.connection.addHandler(
                     $.proxy(this.roster.subscribeToSuggestedItems, this.roster), 

+ 208 - 0
spec/RosterSpec.js

@@ -0,0 +1,208 @@
+(function (root, factory) {
+    define([
+        "converse"
+        ], function (xmppchat) {
+            return factory(xmppchat);
+        }
+    );
+} (this, function (xmppchat) {
+
+    return describe("Contacts Roster", function() {
+
+        // Names from http://www.fakenamegenerator.com/
+        names = [
+            'Louw Spekman', 'Mohamad Stet', 'Dominik Beyer', 'Dirk Eichel', 'Marco Duerr', 'Ute Schiffer',
+            'Billie Westerhuis', 'Sarah Kuester', 'Sabrina Loewe', 'Laura Duerr', 'Mathias Meyer',
+            'Tijm Keller', 'Lea Gerste', 'Martin Pfeffer', 'Ulrike Abt', 'Zoubida van Rooij',
+            'Maylin Hettema', 'Ruwan Bechan', 'Marco Beich', 'Karin Busch', 'Mathias Müller',
+            'Suleyman van Beusichem', 'Nicole Diederich', 'Nanja van Yperen', 'Delany Bloemendaal',
+            'Jannah Hofmeester', 'Christine Trommler', 'Martin Bumgarner', 'Emil Baeten', 'Farshad Brasser',
+            'Gabriele Fisher', 'Sofiane Schopman', 'Sky Wismans', 'Jeffery Stoelwinder', 'Ganesh Waaijenberg',
+            'Dani Boldewijn', 'Katrin Propst', 'Martina Kaiser', 'Philipp Kappel', 'Meeke Grootendorst',
+            'Max Frankfurter', 'Candice van der Knijff', 'Irini Vlastuin', 'Rinse Sommer', 'Annegreet Gomez',
+            'Robin Schook', 'Marcel Eberhardt', 'Simone Brauer', 'Asmaa Haakman', 'Felix Amsel',
+            'Lena Grunewald', 'Laura Grunewald', 'Mandy Seiler', 'Sven Bosch', 'Nuriye Cuypers', 'Ben Zomer',
+            'Leah Weiss', 'Francesca Disseldorp', 'Sven Bumgarner', 'Benjamin Zweig'
+        ];
+
+        describe("contacts roster", function () {
+
+            xmppchat.roster = new xmppchat.RosterItems();
+            xmppchat.rosterview = new xmppchat.RosterView({'model':xmppchat.roster});
+            // stub
+            xmppchat.chatboxesview = {openChat: function () {} };
+            // Hack to make sure there is an element.
+            xmppchat.rosterview.$el = $('<dl id="xmppchat-roster"></dl>');
+            xmppchat.rosterview.render();
+
+            it("should hide the requesting contacts heading if there aren't any", function () {
+                expect(xmppchat.rosterview.$el.find('dt#xmpp-contact-requests').css('display')).toEqual('none');
+            });
+
+            it("should be able to add requesting contacts, and they should be sorted alphabetically", function () {
+                var jid, i, t;
+                spyOn(xmppchat.rosterview, 'render').andCallThrough();
+                spyOn(xmppchat.chatboxesview, 'openChat');
+                for (i=0; i<10; i++) {
+                    jid = names[i].replace(' ','.').toLowerCase() + '@localhost';
+                    xmppchat.roster.addRosterItem(jid, 'none', 'request', names[i]);
+                    expect(xmppchat.rosterview.render).toHaveBeenCalled();
+                    // Check that they are sorted alphabetically
+                    t = xmppchat.rosterview.$el.find('dt#xmpp-contact-requests').siblings('dd.requesting-xmpp-contact').text().replace(/AcceptDecline/g, '');
+                    expect(t).toEqual(names.slice(0,i+1).sort().join(''));
+                    // When a requesting contact is added, the controlbox must
+                    // be opened.
+                    expect(xmppchat.chatboxesview.openChat).toHaveBeenCalledWith('controlbox');
+                }
+            });
+
+            it("should show the requesting contacts heading after they have been added", function () {
+                expect(xmppchat.rosterview.$el.find('dt#xmpp-contact-requests').css('display')).toEqual('block');
+            });
+
+            it("should hide the pending contacts heading if there aren't any", function () {
+                expect(xmppchat.rosterview.$el.find('dt#pending-xmpp-contacts').css('display')).toEqual('none');
+            });
+
+            it("should be able to add pending contacts, and they should be sorted alphabetically", function () {
+                var jid, i, t;
+                spyOn(xmppchat.rosterview, 'render').andCallThrough();
+                for (i=10; i<20; i++) {
+                    jid = names[i].replace(' ','.').toLowerCase() + '@localhost';
+                    xmppchat.roster.addRosterItem(jid, 'none', 'subscribe', names[i]);
+                    expect(xmppchat.rosterview.render).toHaveBeenCalled();
+                    // Check that they are sorted alphabetically
+                    t = xmppchat.rosterview.$el.find('dt#pending-xmpp-contacts').siblings('dd.pending-xmpp-contact').text();
+                    expect(t).toEqual(names.slice(10,i+1).sort().join(''));
+                }
+            });
+
+            it("should show the pending contacts heading after they have been added", function () {
+                expect(xmppchat.rosterview.$el.find('dt#pending-xmpp-contacts').css('display')).toEqual('block');
+            });
+
+            it("should hide the current contacts heading if there aren't any", function () {
+                expect(xmppchat.rosterview.$el.find('dt#xmpp-contacts').css('display')).toEqual('none');
+            });
+
+            it("should be able to add existing contacts, and they should be sorted alphabetically", function () {
+                var jid, i, t;
+                spyOn(xmppchat.rosterview, 'render').andCallThrough();
+                // Add 40 properly regisertered contacts (initially all offline) and check that they are sorted alphabetically
+                for (i=20; i<60; i++) {
+                    jid = names[i].replace(' ','.').toLowerCase() + '@localhost';
+                    xmppchat.roster.addRosterItem(jid, 'both', null, names[i]);
+                    expect(xmppchat.rosterview.render).toHaveBeenCalled();
+                    // Check that they are sorted alphabetically
+                    t = xmppchat.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.offline').find('a.open-chat').text();
+                    expect(t).toEqual(names.slice(20,i+1).sort().join(''));
+                }
+            });
+
+            it("should show the current contacts heading if they have been added", function () {
+                expect(xmppchat.rosterview.$el.find('dt#xmpp-contacts').css('display')).toEqual('block');
+            });
+
+            describe("roster items", function () {
+
+                it("should be able to change their status to online and be sorted alphabetically", function () {
+                    var item, view, jid;
+                    spyOn(xmppchat.rosterview, 'render').andCallThrough();
+                    for (i=59; i>54; i--) {
+                        jid = names[i].replace(' ','.').toLowerCase() + '@localhost';
+                        view = xmppchat.rosterview.rosteritemviews[jid];
+                        spyOn(view, 'render').andCallThrough();
+                        item = view.model;
+                        item.set('presence_type', 'online');
+                        expect(view.render).toHaveBeenCalled();
+                        expect(xmppchat.rosterview.render).toHaveBeenCalled();
+
+                        // Check that they are sorted alphabetically
+                        t = xmppchat.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.online').find('a.open-chat').text();
+                        expect(t).toEqual(names.slice(-(60-i)).sort().join(''));
+                    }
+                });
+
+                it("should be able to change their status to busy and be sorted alphabetically", function () {
+                    var item, view, jid;
+                    spyOn(xmppchat.rosterview, 'render').andCallThrough();
+                    for (i=54; i>49; i--) {
+                        jid = names[i].replace(' ','.').toLowerCase() + '@localhost';
+                        view = xmppchat.rosterview.rosteritemviews[jid];
+                        spyOn(view, 'render').andCallThrough();
+                        item = view.model;
+                        item.set('presence_type', 'busy');
+                        expect(view.render).toHaveBeenCalled();
+                        expect(xmppchat.rosterview.render).toHaveBeenCalled();
+
+                        // Check that they are sorted alphabetically
+                        t = xmppchat.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.busy').find('a.open-chat').text();
+                        expect(t).toEqual(names.slice(-(60-i), -5).sort().join(''));
+                    }
+                });
+
+                it("should be able to change their status to away and be sorted alphabetically", function () {
+                    var item, view, jid;
+                    spyOn(xmppchat.rosterview, 'render').andCallThrough();
+                    for (i=49; i>44; i--) {
+                        jid = names[i].replace(' ','.').toLowerCase() + '@localhost';
+                        view = xmppchat.rosterview.rosteritemviews[jid];
+                        spyOn(view, 'render').andCallThrough();
+                        item = view.model;
+                        item.set('presence_type', 'away');
+                        expect(view.render).toHaveBeenCalled();
+                        expect(xmppchat.rosterview.render).toHaveBeenCalled();
+
+                        // Check that they are sorted alphabetically
+                        t = xmppchat.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.away').find('a.open-chat').text();
+                        expect(t).toEqual(names.slice(-(60-i),-10).sort().join(''));
+                    }
+                });
+
+                it("should be able to change their status to unavailable and be sorted alphabetically", function () {
+                    var item, view, jid;
+                    spyOn(xmppchat.rosterview, 'render').andCallThrough();
+                    for (i=44; i>39; i--) {
+                        jid = names[i].replace(' ','.').toLowerCase() + '@localhost';
+                        view = xmppchat.rosterview.rosteritemviews[jid];
+                        spyOn(view, 'render').andCallThrough();
+                        item = view.model;
+                        item.set('presence_type', 'unavailable');
+                        expect(view.render).toHaveBeenCalled();
+                        expect(xmppchat.rosterview.render).toHaveBeenCalled();
+
+                        // Check that they are sorted alphabetically
+                        t = xmppchat.rosterview.$el.find('dt#xmpp-contacts').siblings('dd.current-xmpp-contact.unavailable').find('a.open-chat').text();
+                        expect(t).toEqual(names.slice(-(60-i), -15).sort().join(''));
+                    }
+                });
+
+                it("should be ordered according to status: online, busy, away, unavailable, offline", function () {
+                    var contacts = xmppchat.rosterview.$el.find('dd.current-xmpp-contact');
+                    var i;
+                    // The first five contacts are online.
+                    for (i=0; i<5; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('online');
+                    }
+                    // The next five are busy
+                    for (i=5; i<10; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('busy');
+                    }
+                    // The next five are away
+                    for (i=10; i<15; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('away');
+                    }
+                    // The next five are unavailable 
+                    for (i=15; i<20; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('unavailable');
+                    }
+                    // The next 20 are offline 
+                    for (i=20; i<40; i++) {
+                        expect($(contacts[i]).attr('class').split(' ',1)[0]).toEqual('offline');
+                    }
+                });
+            });
+
+        });
+    });
+}));

+ 2 - 1
tests_main.js

@@ -1,5 +1,6 @@
 require(["jquery", 
-         "spec/StorageSpec"], function($) {
+         "spec/StorageSpec", 
+         "spec/RosterSpec"], function($) {
 
     $(function($) {
         var jasmineEnv = jasmine.getEnv();