protocol.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. (function (root, factory) {
  2. define([
  3. "jasmine",
  4. "jquery",
  5. "converse-core",
  6. "mock",
  7. "test-utils"], factory);
  8. } (this, function (jasmine, $, converse, mock, test_utils) {
  9. "use strict";
  10. var Strophe = converse.env.Strophe;
  11. var $iq = converse.env.$iq;
  12. var $pres = converse.env.$pres;
  13. var _ = converse.env._;
  14. // See:
  15. // https://xmpp.org/rfcs/rfc3921.html
  16. describe("The Protocol", function () {
  17. describe("Integration of Roster Items and Presence Subscriptions", function () {
  18. // Stub the trimChat method. It causes havoc when running with
  19. // phantomJS.
  20. /* Some level of integration between roster items and presence
  21. * subscriptions is normally expected by an instant messaging user
  22. * regarding the user's subscriptions to and from other contacts. This
  23. * section describes the level of integration that MUST be supported
  24. * within an XMPP instant messaging applications.
  25. *
  26. * There are four primary subscription states:
  27. *
  28. * None -- the user does not have a subscription to the contact's
  29. * presence information, and the contact does not have a subscription
  30. * to the user's presence information
  31. * To -- the user has a subscription to the contact's presence
  32. * information, but the contact does not have a subscription to the
  33. * user's presence information
  34. * From -- the contact has a subscription to the user's presence
  35. * information, but the user does not have a subscription to the
  36. * contact's presence information
  37. * Both -- both the user and the contact have subscriptions to each
  38. * other's presence information (i.e., the union of 'from' and 'to')
  39. *
  40. * Each of these states is reflected in the roster of both the user and
  41. * the contact, thus resulting in durable subscription states.
  42. *
  43. * The 'from' and 'to' addresses are OPTIONAL in roster pushes; if
  44. * included, their values SHOULD be the full JID of the resource for
  45. * that session. A client MUST acknowledge each roster push with an IQ
  46. * stanza of type "result".
  47. */
  48. it("Subscribe to contact, contact accepts and subscribes back",
  49. mock.initConverseWithPromises(
  50. null, ['rosterGroupsFetched'],
  51. { roster_groups: false },
  52. function (done, _converse) {
  53. var contact, sent_stanza, IQ_id, stanza;
  54. test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
  55. .then(function () {
  56. return test_utils.waitUntil(function () {
  57. return _converse.xmppstatus.get('fullname');
  58. }, 300);
  59. }).then(function () {
  60. /* The process by which a user subscribes to a contact, including
  61. * the interaction between roster items and subscription states.
  62. */
  63. test_utils.openControlBox(_converse);
  64. var panel = _converse.chatboxviews.get('controlbox').contactspanel;
  65. spyOn(panel, "addContactFromForm").and.callThrough();
  66. spyOn(_converse.roster, "addAndSubscribe").and.callThrough();
  67. spyOn(_converse.roster, "addContact").and.callThrough();
  68. spyOn(_converse.roster, "sendContactAddIQ").and.callThrough();
  69. spyOn(_converse.api.vcard, "get").and.callThrough();
  70. var sendIQ = _converse.connection.sendIQ;
  71. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  72. sent_stanza = iq;
  73. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  74. });
  75. panel.delegateEvents(); // Rebind all events so that our spy gets called
  76. /* Add a new contact through the UI */
  77. var form = panel.el.querySelector('form.add-xmpp-contact');
  78. expect(_.isNull(form)).toBeTruthy();
  79. // Click the "Add a contact" link.
  80. panel.$('.toggle-xmpp-contact-form').click();
  81. // Check that the form appears
  82. form = panel.el.querySelector('form.add-xmpp-contact');
  83. expect(form.parentElement.offsetHeight).not.toBe(0);
  84. expect(_.includes(form.parentElement.classList, 'collapsed')).toBeFalsy();
  85. // Fill in the form and submit
  86. $(form).find('input').val('contact@example.org');
  87. $(form).submit();
  88. /* In preparation for being able to render the contact in the
  89. * user's client interface and for the server to keep track of the
  90. * subscription, the user's client SHOULD perform a "roster set"
  91. * for the new roster item.
  92. */
  93. expect(panel.addContactFromForm).toHaveBeenCalled();
  94. expect(_converse.roster.addAndSubscribe).toHaveBeenCalled();
  95. expect(_converse.roster.addContact).toHaveBeenCalled();
  96. // The form should not be visible anymore (by virtue of its
  97. // parent being collapsed)
  98. expect(form.parentElement.offsetHeight).toBe(0);
  99. expect(_.includes(form.parentElement.classList, 'collapsed')).toBeTrue;
  100. /* _converse request consists of sending an IQ
  101. * stanza of type='set' containing a <query/> element qualified by
  102. * the 'jabber:iq:roster' namespace, which in turn contains an
  103. * <item/> element that defines the new roster item; the <item/>
  104. * element MUST possess a 'jid' attribute, MAY possess a 'name'
  105. * attribute, MUST NOT possess a 'subscription' attribute, and MAY
  106. * contain one or more <group/> child elements:
  107. *
  108. * <iq type='set' id='set1'>
  109. * <query xmlns='jabber:iq:roster'>
  110. * <item
  111. * jid='contact@example.org'
  112. * name='MyContact'>
  113. * <group>MyBuddies</group>
  114. * </item>
  115. * </query>
  116. * </iq>
  117. */
  118. expect(_converse.roster.sendContactAddIQ).toHaveBeenCalled();
  119. expect(sent_stanza.toLocaleString()).toBe(
  120. "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
  121. "<query xmlns='jabber:iq:roster'>"+
  122. "<item jid='contact@example.org' name='contact@example.org'/>"+
  123. "</query>"+
  124. "</iq>"
  125. );
  126. /* As a result, the user's server (1) MUST initiate a roster push
  127. * for the new roster item to all available resources associated
  128. * with _converse user that have requested the roster, setting the
  129. * 'subscription' attribute to a value of "none"; and (2) MUST
  130. * reply to the sending resource with an IQ result indicating the
  131. * success of the roster set:
  132. *
  133. * <iq type='set'>
  134. * <query xmlns='jabber:iq:roster'>
  135. * <item
  136. * jid='contact@example.org'
  137. * subscription='none'
  138. * name='MyContact'>
  139. * <group>MyBuddies</group>
  140. * </item>
  141. * </query>
  142. * </iq>
  143. */
  144. var create = _converse.roster.create;
  145. var sent_stanzas = [];
  146. spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
  147. sent_stanza = stanza;
  148. sent_stanzas.push(stanza.toLocaleString());
  149. });
  150. spyOn(_converse.roster, 'create').and.callFake(function () {
  151. contact = create.apply(_converse.roster, arguments);
  152. spyOn(contact, 'subscribe').and.callThrough();
  153. return contact;
  154. });
  155. stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
  156. .c('item', {
  157. 'jid': 'contact@example.org',
  158. 'subscription': 'none',
  159. 'name': 'contact@example.org'});
  160. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  161. /*
  162. * <iq type='result' id='set1'/>
  163. */
  164. stanza = $iq({'type': 'result', 'id':IQ_id});
  165. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  166. // A contact should now have been created
  167. expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy();
  168. expect(contact.get('jid')).toBe('contact@example.org');
  169. expect(_converse.api.vcard.get).toHaveBeenCalled();
  170. /* To subscribe to the contact's presence information,
  171. * the user's client MUST send a presence stanza of
  172. * type='subscribe' to the contact:
  173. *
  174. * <presence to='contact@example.org' type='subscribe'/>
  175. */
  176. return test_utils.waitUntil(function () {
  177. return sent_stanzas.length == 1;
  178. }, 300);
  179. }).then(function () {
  180. expect(contact.subscribe).toHaveBeenCalled();
  181. expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
  182. "<presence to='contact@example.org' type='subscribe' xmlns='jabber:client'>"+
  183. "<nick xmlns='http://jabber.org/protocol/nick'>Max Mustermann</nick>"+
  184. "</presence>"
  185. );
  186. /* As a result, the user's server MUST initiate a second roster
  187. * push to all of the user's available resources that have
  188. * requested the roster, setting the contact to the pending
  189. * sub-state of the 'none' subscription state; _converse pending
  190. * sub-state is denoted by the inclusion of the ask='subscribe'
  191. * attribute in the roster item:
  192. *
  193. * <iq type='set'>
  194. * <query xmlns='jabber:iq:roster'>
  195. * <item
  196. * jid='contact@example.org'
  197. * subscription='none'
  198. * ask='subscribe'
  199. * name='MyContact'>
  200. * <group>MyBuddies</group>
  201. * </item>
  202. * </query>
  203. * </iq>
  204. */
  205. spyOn(_converse.roster, "updateContact").and.callThrough();
  206. stanza = $iq({'type': 'set', 'from': 'dummy@localhost'})
  207. .c('query', {'xmlns': 'jabber:iq:roster'})
  208. .c('item', {
  209. 'jid': 'contact@example.org',
  210. 'subscription': 'none',
  211. 'ask': 'subscribe',
  212. 'name': 'contact@example.org'});
  213. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  214. expect(_converse.roster.updateContact).toHaveBeenCalled();
  215. // Check that the user is now properly shown as a pending
  216. // contact in the roster.
  217. var $header = $('a:contains("Pending contacts")');
  218. return test_utils.waitUntil(function () {
  219. return $('a:contains("Pending contacts")').length && $header.is(":visible");
  220. }, 300);
  221. }).then(function () {
  222. var $header = $('a:contains("Pending contacts")');
  223. var $contacts = $header.parent().find('li');
  224. expect($contacts.length).toBe(1);
  225. expect($contacts.is(':visible')).toBeTruthy();
  226. spyOn(contact, "ackSubscribe").and.callThrough();
  227. /* Here we assume the "happy path" that the contact
  228. * approves the subscription request
  229. *
  230. * <presence
  231. * to='user@example.com'
  232. * from='contact@example.org'
  233. * type='subscribed'/>
  234. */
  235. stanza = $pres({
  236. 'to': _converse.bare_jid,
  237. 'from': 'contact@example.org',
  238. 'type': 'subscribed'
  239. });
  240. sent_stanza = ""; // Reset
  241. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  242. /* Upon receiving the presence stanza of type "subscribed",
  243. * the user SHOULD acknowledge receipt of that
  244. * subscription state notification by sending a presence
  245. * stanza of type "subscribe".
  246. */
  247. expect(contact.ackSubscribe).toHaveBeenCalled();
  248. expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
  249. "<presence type='subscribe' to='contact@example.org' xmlns='jabber:client'/>"
  250. );
  251. /* The user's server MUST initiate a roster push to all of the user's
  252. * available resources that have requested the roster,
  253. * containing an updated roster item for the contact with
  254. * the 'subscription' attribute set to a value of "to";
  255. *
  256. * <iq type='set'>
  257. * <query xmlns='jabber:iq:roster'>
  258. * <item
  259. * jid='contact@example.org'
  260. * subscription='to'
  261. * name='MyContact'>
  262. * <group>MyBuddies</group>
  263. * </item>
  264. * </query>
  265. * </iq>
  266. */
  267. IQ_id = _converse.connection.getUniqueId('roster');
  268. stanza = $iq({'type': 'set', 'id': IQ_id})
  269. .c('query', {'xmlns': 'jabber:iq:roster'})
  270. .c('item', {
  271. 'jid': 'contact@example.org',
  272. 'subscription': 'to',
  273. 'name': 'contact@example.org'});
  274. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  275. // Check that the IQ set was acknowledged.
  276. expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec)
  277. "<iq type='result' id='"+IQ_id+"' from='dummy@localhost/resource' xmlns='jabber:client'/>"
  278. );
  279. expect(_converse.roster.updateContact).toHaveBeenCalled();
  280. // The contact should now be visible as an existing
  281. // contact (but still offline).
  282. $header = $('a:contains("My contacts")');
  283. expect($header.length).toBe(1);
  284. expect($header.is(":visible")).toBeTruthy();
  285. $contacts = $header.parent().find('li');
  286. expect($contacts.length).toBe(1);
  287. // Check that it has the right classes and text
  288. expect($contacts.hasClass('to')).toBeTruthy();
  289. expect($contacts.hasClass('both')).toBeFalsy();
  290. expect($contacts.hasClass('current-xmpp-contact')).toBeTruthy();
  291. expect($contacts.text().trim()).toBe('Contact');
  292. expect(contact.get('chat_status')).toBe('offline');
  293. /* <presence
  294. * from='contact@example.org/resource'
  295. * to='user@example.com/resource'/>
  296. */
  297. stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'});
  298. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  299. // Now the contact should also be online.
  300. expect(contact.get('chat_status')).toBe('online');
  301. /* Section 8.3. Creating a Mutual Subscription
  302. *
  303. * If the contact wants to create a mutual subscription,
  304. * the contact MUST send a subscription request to the
  305. * user.
  306. *
  307. * <presence from='contact@example.org' to='user@example.com' type='subscribe'/>
  308. */
  309. spyOn(contact, 'authorize').and.callThrough();
  310. spyOn(_converse.roster, 'handleIncomingSubscription').and.callThrough();
  311. stanza = $pres({
  312. 'to': _converse.bare_jid,
  313. 'from': 'contact@example.org/resource',
  314. 'type': 'subscribe'});
  315. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  316. expect(_converse.roster.handleIncomingSubscription).toHaveBeenCalled();
  317. /* The user's client MUST send a presence stanza of type
  318. * "subscribed" to the contact in order to approve the
  319. * subscription request.
  320. *
  321. * <presence to='contact@example.org' type='subscribed'/>
  322. */
  323. expect(contact.authorize).toHaveBeenCalled();
  324. expect(sent_stanza.toLocaleString()).toBe(
  325. "<presence to='contact@example.org' type='subscribed' xmlns='jabber:client'/>"
  326. );
  327. /* As a result, the user's server MUST initiate a
  328. * roster push containing a roster item for the
  329. * contact with the 'subscription' attribute set to
  330. * a value of "both".
  331. *
  332. * <iq type='set'>
  333. * <query xmlns='jabber:iq:roster'>
  334. * <item
  335. * jid='contact@example.org'
  336. * subscription='both'
  337. * name='MyContact'>
  338. * <group>MyBuddies</group>
  339. * </item>
  340. * </query>
  341. * </iq>
  342. */
  343. stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
  344. .c('item', {
  345. 'jid': 'contact@example.org',
  346. 'subscription': 'both',
  347. 'name': 'contact@example.org'});
  348. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  349. expect(_converse.roster.updateContact).toHaveBeenCalled();
  350. // The class on the contact will now have switched.
  351. expect($contacts.hasClass('to')).toBeFalsy();
  352. expect($contacts.hasClass('both')).toBeTruthy();
  353. done();
  354. });
  355. }));
  356. it("Alternate Flow: Contact Declines Subscription Request",
  357. mock.initConverseWithPromises(
  358. null, ['rosterGroupsFetched'], {},
  359. function (done, _converse) {
  360. /* The process by which a user subscribes to a contact, including
  361. * the interaction between roster items and subscription states.
  362. */
  363. var contact, stanza, sent_stanza, sent_IQ;
  364. test_utils.openControlBox(_converse);
  365. // Add a new roster contact via roster push
  366. stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'})
  367. .c('item', {
  368. 'jid': 'contact@example.org',
  369. 'subscription': 'none',
  370. 'ask': 'subscribe',
  371. 'name': 'contact@example.org'});
  372. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  373. // A pending contact should now exist.
  374. contact = _converse.roster.get('contact@example.org');
  375. expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy();
  376. spyOn(contact, "ackUnsubscribe").and.callThrough();
  377. spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
  378. sent_stanza = stanza;
  379. });
  380. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  381. sent_IQ = iq;
  382. });
  383. /* We now assume the contact declines the subscription
  384. * requests.
  385. *
  386. * Upon receiving the presence stanza of type "unsubscribed"
  387. * addressed to the user, the user's server (1) MUST deliver
  388. * that presence stanza to the user and (2) MUST initiate a
  389. * roster push to all of the user's available resources that
  390. * have requested the roster, containing an updated roster
  391. * item for the contact with the 'subscription' attribute
  392. * set to a value of "none" and with no 'ask' attribute:
  393. *
  394. * <presence
  395. * from='contact@example.org'
  396. * to='user@example.com'
  397. * type='unsubscribed'/>
  398. *
  399. * <iq type='set'>
  400. * <query xmlns='jabber:iq:roster'>
  401. * <item
  402. * jid='contact@example.org'
  403. * subscription='none'
  404. * name='MyContact'>
  405. * <group>MyBuddies</group>
  406. * </item>
  407. * </query>
  408. * </iq>
  409. */
  410. // FIXME: also add the <iq>
  411. stanza = $pres({
  412. 'to': _converse.bare_jid,
  413. 'from': 'contact@example.org',
  414. 'type': 'unsubscribed'
  415. });
  416. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  417. /* Upon receiving the presence stanza of type "unsubscribed",
  418. * the user SHOULD acknowledge receipt of that subscription
  419. * state notification through either "affirming" it by
  420. * sending a presence stanza of type "unsubscribe
  421. */
  422. expect(contact.ackUnsubscribe).toHaveBeenCalled();
  423. expect(sent_stanza.toLocaleString()).toBe(
  424. "<presence type='unsubscribe' to='contact@example.org' xmlns='jabber:client'/>"
  425. );
  426. /* _converse.js will then also automatically remove the
  427. * contact from the user's roster.
  428. */
  429. expect(sent_IQ.toLocaleString()).toBe(
  430. "<iq type='set' xmlns='jabber:client'>"+
  431. "<query xmlns='jabber:iq:roster'>"+
  432. "<item jid='contact@example.org' subscription='remove'/>"+
  433. "</query>"+
  434. "</iq>"
  435. );
  436. done();
  437. }));
  438. it("Unsubscribe to a contact when subscription is mutual",
  439. mock.initConverseWithPromises(
  440. null, ['rosterGroupsFetched'],
  441. { roster_groups: false },
  442. function (done, _converse) {
  443. var sent_IQ, IQ_id, jid = 'annegreet.gomez@localhost';
  444. test_utils.openControlBox(_converse);
  445. test_utils.createContacts(_converse, 'current');
  446. spyOn(window, 'confirm').and.returnValue(true);
  447. // We now have a contact we want to remove
  448. expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy();
  449. var sendIQ = _converse.connection.sendIQ;
  450. spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
  451. sent_IQ = iq;
  452. IQ_id = sendIQ.bind(this)(iq, callback, errback);
  453. });
  454. test_utils.waitUntil(function () {
  455. return $('a:contains("My contacts")').length;
  456. }).then(function () {
  457. var $header = $('a:contains("My contacts")');
  458. // remove the first user
  459. $($header.parent().find('li .remove-xmpp-contact').get(0)).click();
  460. expect(window.confirm).toHaveBeenCalled();
  461. /* Section 8.6 Removing a Roster Item and Cancelling All
  462. * Subscriptions
  463. *
  464. * First the user is removed from the roster
  465. * Because there may be many steps involved in completely
  466. * removing a roster item and cancelling subscriptions in
  467. * both directions, the roster management protocol includes
  468. * a "shortcut" method for doing so. The process may be
  469. * initiated no matter what the current subscription state
  470. * is by sending a roster set containing an item for the
  471. * contact with the 'subscription' attribute set to a value
  472. * of "remove":
  473. *
  474. * <iq type='set' id='remove1'>
  475. * <query xmlns='jabber:iq:roster'>
  476. * <item jid='contact@example.org' subscription='remove'/>
  477. * </query>
  478. * </iq>
  479. */
  480. expect(sent_IQ.toLocaleString()).toBe(
  481. "<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
  482. "<query xmlns='jabber:iq:roster'>"+
  483. "<item jid='annegreet.gomez@localhost' subscription='remove'/>"+
  484. "</query>"+
  485. "</iq>");
  486. // Receive confirmation from the contact's server
  487. // <iq type='result' id='remove1'/>
  488. var stanza = $iq({'type': 'result', 'id':IQ_id});
  489. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  490. // Our contact has now been removed
  491. expect(typeof _converse.roster.get(jid) === "undefined").toBeTruthy();
  492. done();
  493. });
  494. }));
  495. it("Receiving a subscription request", mock.initConverseWithPromises(
  496. null, ['rosterGroupsFetched'], {},
  497. function (done, _converse) {
  498. spyOn(_converse, "emit");
  499. test_utils.openControlBox(_converse);
  500. test_utils.createContacts(_converse, 'current'); // Create some contacts so that we can test positioning
  501. /* <presence
  502. * from='user@example.com'
  503. * to='contact@example.org'
  504. * type='subscribe'/>
  505. */
  506. var stanza = $pres({
  507. 'to': _converse.bare_jid,
  508. 'from': 'contact@example.org',
  509. 'type': 'subscribe'
  510. }).c('nick', {
  511. 'xmlns': Strophe.NS.NICK,
  512. }).t('Clint Contact');
  513. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  514. test_utils.waitUntil(function () {
  515. return $('a:contains("Contact requests")').length;
  516. }).then(function () {
  517. expect(_converse.emit).toHaveBeenCalledWith('contactRequest', jasmine.any(Object));
  518. var $header = $('a:contains("Contact requests")');
  519. expect($header.length).toBe(1);
  520. expect($header.is(":visible")).toBeTruthy();
  521. var $contacts = $header.parent().find('li');
  522. expect($contacts.length).toBe(1);
  523. done();
  524. });
  525. }));
  526. });
  527. });
  528. }));