protocol.js 29 KB

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