autocomplete.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. /*global mock, converse */
  2. const $pres = converse.env.$pres;
  3. const $msg = converse.env.$msg;
  4. const Strophe = converse.env.Strophe;
  5. const u = converse.env.utils;
  6. describe("The nickname autocomplete feature", function () {
  7. it("shows all autocompletion options when the user presses @",
  8. mock.initConverse(
  9. ['rosterContactsFetched', 'chatBoxesFetched'], {},
  10. async function (done, _converse) {
  11. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
  12. const view = _converse.chatboxviews.get('lounge@montague.lit');
  13. // Nicknames from presences
  14. ['dick', 'harry'].forEach((nick) => {
  15. _converse.connection._dataRecv(mock.createRequest(
  16. $pres({
  17. 'to': 'tom@montague.lit/resource',
  18. 'from': `lounge@montague.lit/${nick}`
  19. })
  20. .c('x', {xmlns: Strophe.NS.MUC_USER})
  21. .c('item', {
  22. 'affiliation': 'none',
  23. 'jid': `${nick}@montague.lit/resource`,
  24. 'role': 'participant'
  25. })));
  26. });
  27. // Nicknames from messages
  28. const msg = $msg({
  29. from: 'lounge@montague.lit/jane',
  30. id: u.getUniqueId(),
  31. to: 'romeo@montague.lit',
  32. type: 'groupchat'
  33. }).c('body').t('Hello world').tree();
  34. await view.model.handleMessageStanza(msg);
  35. await u.waitUntil(() => view.model.messages.last()?.get('received'));
  36. // Test that pressing @ brings up all options
  37. const textarea = view.querySelector('textarea.chat-textarea');
  38. const at_event = {
  39. 'target': textarea,
  40. 'preventDefault': function preventDefault () {},
  41. 'stopPropagation': function stopPropagation () {},
  42. 'keyCode': 50,
  43. 'key': '@'
  44. };
  45. view.onKeyDown(at_event);
  46. textarea.value = '@';
  47. view.onKeyUp(at_event);
  48. await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
  49. expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
  50. expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
  51. expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
  52. expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
  53. done();
  54. }));
  55. it("shows all autocompletion options when the user presses @ right after a new line",
  56. mock.initConverse(
  57. ['rosterContactsFetched', 'chatBoxesFetched'], {},
  58. async function (done, _converse) {
  59. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
  60. const view = _converse.chatboxviews.get('lounge@montague.lit');
  61. // Nicknames from presences
  62. ['dick', 'harry'].forEach((nick) => {
  63. _converse.connection._dataRecv(mock.createRequest(
  64. $pres({
  65. 'to': 'tom@montague.lit/resource',
  66. 'from': `lounge@montague.lit/${nick}`
  67. })
  68. .c('x', {xmlns: Strophe.NS.MUC_USER})
  69. .c('item', {
  70. 'affiliation': 'none',
  71. 'jid': `${nick}@montague.lit/resource`,
  72. 'role': 'participant'
  73. })));
  74. });
  75. // Nicknames from messages
  76. const msg = $msg({
  77. from: 'lounge@montague.lit/jane',
  78. id: u.getUniqueId(),
  79. to: 'romeo@montague.lit',
  80. type: 'groupchat'
  81. }).c('body').t('Hello world').tree();
  82. await view.model.handleMessageStanza(msg);
  83. await u.waitUntil(() => view.model.messages.last()?.get('received'));
  84. // Test that pressing @ brings up all options
  85. const textarea = view.querySelector('textarea.chat-textarea');
  86. const at_event = {
  87. 'target': textarea,
  88. 'preventDefault': function preventDefault () {},
  89. 'stopPropagation': function stopPropagation () {},
  90. 'keyCode': 50,
  91. 'key': '@'
  92. };
  93. textarea.value = '\n'
  94. view.onKeyDown(at_event);
  95. textarea.value = '\n@';
  96. view.onKeyUp(at_event);
  97. await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
  98. expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
  99. expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
  100. expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
  101. expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
  102. done();
  103. }));
  104. it("shows all autocompletion options when the user presses @ right after an allowed character",
  105. mock.initConverse(
  106. ['rosterContactsFetched', 'chatBoxesFetched'], {'opening_mention_characters':['(']},
  107. async function (done, _converse) {
  108. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
  109. const view = _converse.chatboxviews.get('lounge@montague.lit');
  110. // Nicknames from presences
  111. ['dick', 'harry'].forEach((nick) => {
  112. _converse.connection._dataRecv(mock.createRequest(
  113. $pres({
  114. 'to': 'tom@montague.lit/resource',
  115. 'from': `lounge@montague.lit/${nick}`
  116. })
  117. .c('x', {xmlns: Strophe.NS.MUC_USER})
  118. .c('item', {
  119. 'affiliation': 'none',
  120. 'jid': `${nick}@montague.lit/resource`,
  121. 'role': 'participant'
  122. })));
  123. });
  124. // Nicknames from messages
  125. const msg = $msg({
  126. from: 'lounge@montague.lit/jane',
  127. id: u.getUniqueId(),
  128. to: 'romeo@montague.lit',
  129. type: 'groupchat'
  130. }).c('body').t('Hello world').tree();
  131. await view.model.handleMessageStanza(msg);
  132. await u.waitUntil(() => view.model.messages.last()?.get('received'));
  133. // Test that pressing @ brings up all options
  134. const textarea = view.querySelector('textarea.chat-textarea');
  135. const at_event = {
  136. 'target': textarea,
  137. 'preventDefault': function preventDefault () {},
  138. 'stopPropagation': function stopPropagation () {},
  139. 'keyCode': 50,
  140. 'key': '@'
  141. };
  142. textarea.value = '('
  143. view.onKeyDown(at_event);
  144. textarea.value = '(@';
  145. view.onKeyUp(at_event);
  146. await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
  147. expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
  148. expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry');
  149. expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane');
  150. expect(view.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom');
  151. done();
  152. }));
  153. it("should order by query index position and length", mock.initConverse(
  154. ['rosterContactsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
  155. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom');
  156. const view = _converse.chatboxviews.get('lounge@montague.lit');
  157. // Nicknames from presences
  158. ['bernard', 'naber', 'helberlo', 'john', 'jones'].forEach((nick) => {
  159. _converse.connection._dataRecv(mock.createRequest(
  160. $pres({
  161. 'to': 'tom@montague.lit/resource',
  162. 'from': `lounge@montague.lit/${nick}`
  163. })
  164. .c('x', { xmlns: Strophe.NS.MUC_USER })
  165. .c('item', {
  166. 'affiliation': 'none',
  167. 'jid': `${nick}@montague.lit/resource`,
  168. 'role': 'participant'
  169. })));
  170. });
  171. const textarea = view.querySelector('textarea.chat-textarea');
  172. const at_event = {
  173. 'target': textarea,
  174. 'preventDefault': function preventDefault() { },
  175. 'stopPropagation': function stopPropagation() { },
  176. 'keyCode': 50,
  177. 'key': '@'
  178. };
  179. // Test that results are sorted by query index
  180. view.onKeyDown(at_event);
  181. textarea.value = '@ber';
  182. view.onKeyUp(at_event);
  183. await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 3);
  184. expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('bernard');
  185. expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('naber');
  186. expect(view.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('helberlo');
  187. // Test that when the query index is equal, results should be sorted by length
  188. textarea.value = '@jo';
  189. view.onKeyUp(at_event);
  190. await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 2);
  191. expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('john');
  192. expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('jones');
  193. done();
  194. }));
  195. it("autocompletes when the user presses tab",
  196. mock.initConverse(
  197. ['rosterContactsFetched', 'chatBoxesFetched'], {},
  198. async function (done, _converse) {
  199. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
  200. const view = _converse.chatboxviews.get('lounge@montague.lit');
  201. expect(view.model.occupants.length).toBe(1);
  202. let presence = $pres({
  203. 'to': 'romeo@montague.lit/orchard',
  204. 'from': 'lounge@montague.lit/some1'
  205. })
  206. .c('x', {xmlns: Strophe.NS.MUC_USER})
  207. .c('item', {
  208. 'affiliation': 'none',
  209. 'jid': 'some1@montague.lit/resource',
  210. 'role': 'participant'
  211. });
  212. _converse.connection._dataRecv(mock.createRequest(presence));
  213. expect(view.model.occupants.length).toBe(2);
  214. const textarea = view.querySelector('textarea.chat-textarea');
  215. textarea.value = "hello som";
  216. // Press tab
  217. const tab_event = {
  218. 'target': textarea,
  219. 'preventDefault': function preventDefault () {},
  220. 'stopPropagation': function stopPropagation () {},
  221. 'keyCode': 9,
  222. 'key': 'Tab'
  223. }
  224. view.onKeyDown(tab_event);
  225. view.onKeyUp(tab_event);
  226. await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
  227. expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
  228. expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1');
  229. const backspace_event = {
  230. 'target': textarea,
  231. 'preventDefault': function preventDefault () {},
  232. 'keyCode': 8
  233. }
  234. for (var i=0; i<3; i++) {
  235. // Press backspace 3 times to remove "som"
  236. view.onKeyDown(backspace_event);
  237. textarea.value = textarea.value.slice(0, textarea.value.length-1)
  238. view.onKeyUp(backspace_event);
  239. }
  240. await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === true);
  241. presence = $pres({
  242. 'to': 'romeo@montague.lit/orchard',
  243. 'from': 'lounge@montague.lit/some2'
  244. })
  245. .c('x', {xmlns: Strophe.NS.MUC_USER})
  246. .c('item', {
  247. 'affiliation': 'none',
  248. 'jid': 'some2@montague.lit/resource',
  249. 'role': 'participant'
  250. });
  251. _converse.connection._dataRecv(mock.createRequest(presence));
  252. textarea.value = "hello s s";
  253. view.onKeyDown(tab_event);
  254. view.onKeyUp(tab_event);
  255. await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
  256. expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2);
  257. const up_arrow_event = {
  258. 'target': textarea,
  259. 'preventDefault': () => (up_arrow_event.defaultPrevented = true),
  260. 'stopPropagation': function stopPropagation () {},
  261. 'keyCode': 38
  262. }
  263. view.onKeyDown(up_arrow_event);
  264. view.onKeyUp(up_arrow_event);
  265. expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2);
  266. expect(view.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1');
  267. expect(view.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2');
  268. view.onKeyDown({
  269. 'target': textarea,
  270. 'preventDefault': function preventDefault () {},
  271. 'stopPropagation': function stopPropagation () {},
  272. 'keyCode': 13 // Enter
  273. });
  274. expect(textarea.value).toBe('hello s @some2 ');
  275. // Test that pressing tab twice selects
  276. presence = $pres({
  277. 'to': 'romeo@montague.lit/orchard',
  278. 'from': 'lounge@montague.lit/z3r0'
  279. })
  280. .c('x', {xmlns: Strophe.NS.MUC_USER})
  281. .c('item', {
  282. 'affiliation': 'none',
  283. 'jid': 'z3r0@montague.lit/resource',
  284. 'role': 'participant'
  285. });
  286. _converse.connection._dataRecv(mock.createRequest(presence));
  287. textarea.value = "hello z";
  288. view.onKeyDown(tab_event);
  289. view.onKeyUp(tab_event);
  290. await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
  291. view.onKeyDown(tab_event);
  292. view.onKeyUp(tab_event);
  293. await u.waitUntil(() => textarea.value === 'hello @z3r0 ');
  294. done();
  295. }));
  296. it("autocompletes when the user presses backspace",
  297. mock.initConverse(
  298. ['rosterContactsFetched'], {},
  299. async function (done, _converse) {
  300. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
  301. const view = _converse.chatboxviews.get('lounge@montague.lit');
  302. expect(view.model.occupants.length).toBe(1);
  303. const presence = $pres({
  304. 'to': 'romeo@montague.lit/orchard',
  305. 'from': 'lounge@montague.lit/some1'
  306. })
  307. .c('x', {xmlns: Strophe.NS.MUC_USER})
  308. .c('item', {
  309. 'affiliation': 'none',
  310. 'jid': 'some1@montague.lit/resource',
  311. 'role': 'participant'
  312. });
  313. _converse.connection._dataRecv(mock.createRequest(presence));
  314. expect(view.model.occupants.length).toBe(2);
  315. const textarea = view.querySelector('textarea.chat-textarea');
  316. textarea.value = "hello @some1 ";
  317. // Press backspace
  318. const backspace_event = {
  319. 'target': textarea,
  320. 'preventDefault': function preventDefault () {},
  321. 'stopPropagation': function stopPropagation () {},
  322. 'keyCode': 8,
  323. 'key': 'Backspace'
  324. }
  325. view.onKeyDown(backspace_event);
  326. textarea.value = "hello @some1"; // Mimic backspace
  327. view.onKeyUp(backspace_event);
  328. await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
  329. expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
  330. expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1');
  331. done();
  332. }));
  333. });