autocomplete.js 16 KB

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