filters.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. afterEach(function () {
  2. jasmine.clock().uninstall();
  3. });
  4. describe('Filters', function() {
  5. let $filter;
  6. // Ignoring page reload request
  7. beforeAll(() => window.onbeforeunload = () => null);
  8. let webClientServiceMock = {
  9. me: {
  10. id: 'MEMEMEME',
  11. displayName: 'Er'
  12. },
  13. contacts: {
  14. get: function(id) {
  15. if (id === 'AAAAAAAA') {
  16. return {
  17. displayName: 'ContactA'
  18. }
  19. }
  20. else if (id === 'XXXXXXXX') {
  21. return {
  22. displayName: 'ContactX'
  23. }
  24. }
  25. else if (id === '*AAAAAAA') {
  26. return {
  27. displayName: 'GWContactA'
  28. }
  29. }
  30. else if (id === 'BAD0BAD1') {
  31. return {
  32. displayName: '<b>< script >foo&ndash;</b>< script>',
  33. }
  34. }
  35. return null;
  36. }
  37. }
  38. };
  39. let translationMock = {
  40. instant: function(label) {
  41. return label;
  42. }
  43. };
  44. beforeEach(function() {
  45. module('3ema.services');
  46. module('3ema.filters');
  47. module(function($provide) {
  48. $provide.value('WebClientService', webClientServiceMock);
  49. $provide.value('$translate', translationMock);
  50. $provide.constant('$state', null);
  51. });
  52. // Inject the $filter function
  53. inject(function(_$filter_) {
  54. $filter = _$filter_;
  55. });
  56. });
  57. function testPatterns(filterName, cases) {
  58. const filter = $filter(filterName);
  59. for (let testcase of cases) {
  60. const input = testcase[0];
  61. const expected = testcase[1];
  62. expect(filter(input)).toEqual(expected);
  63. };
  64. };
  65. describe('markify', function() {
  66. this.testPatterns = (cases) => testPatterns('markify', cases);
  67. it('detects bold text', () => {
  68. this.testPatterns([
  69. ['*bold text (not italic)*',
  70. '<span class="text-bold">bold text (not italic)</span>'],
  71. ]);
  72. });
  73. it('detects italic text', () => {
  74. this.testPatterns([
  75. ['This text is not italic.',
  76. 'This text is not italic.'],
  77. ['_This text is italic._',
  78. '<span class="text-italic">This text is italic.</span>'],
  79. ['This text is _partially_ italic',
  80. 'This text is <span class="text-italic">partially</span> italic'],
  81. ['This text has _two_ _italic_ bits',
  82. 'This text has <span class="text-italic">two</span> <span class="text-italic">italic</span> bits'],
  83. ]);
  84. });
  85. it('detects strikethrough text', () => {
  86. this.testPatterns([
  87. ['so ~strikethrough~', 'so <span class="text-strike">strikethrough</span>'],
  88. ]);
  89. });
  90. it('detects mixed markup', () => {
  91. this.testPatterns([
  92. ['*bold text with _italic_ *',
  93. '<span class="text-bold">bold text with <span class="text-italic">italic</span> </span>'],
  94. ['*part bold,* _part italic_',
  95. '<span class="text-bold">part bold,</span> <span class="text-italic">part italic</span>'],
  96. ['_italic text with *bold* _',
  97. '<span class="text-italic">italic text with <span class="text-bold">bold</span> </span>'],
  98. ]);
  99. });
  100. it('is only applied on word boundaries', () => {
  101. this.testPatterns([
  102. ['so not_really_italic',
  103. 'so not_really_italic'],
  104. ['invalid*bold*stuff',
  105. 'invalid*bold*stuff'],
  106. ['no~strike~through',
  107. 'no~strike~through'],
  108. ['*bold_but_no~strike~through*',
  109. '<span class="text-bold">bold_but_no~strike~through</span>'],
  110. ]);
  111. });
  112. it('does not break URLs', () => {
  113. this.testPatterns([
  114. ['https://en.wikipedia.org/wiki/Java_class_file *nice*',
  115. 'https://en.wikipedia.org/wiki/Java_class_file <span class="text-bold">nice</span>'],
  116. ['<a href="https://threema.ch/>_Threema_</a>',
  117. '<a href="https://threema.ch/><span class="text-italic">Threema</span></a>'],
  118. ]);
  119. });
  120. it('ignores invalid markup', () => {
  121. this.testPatterns([
  122. ['*invalid markup (do not parse)_', '*invalid markup (do not parse)_'],
  123. ['random *asterisk', 'random *asterisk'],
  124. ]);
  125. });
  126. it('ignores markup with \\n (newline)', () => {
  127. this.testPatterns([
  128. ['*First line\n and a new one. (do not parse)*', '*First line\n and a new one. (do not parse)*'],
  129. ['*\nbegins with linebreak. (do not parse)*', '*\nbegins with linebreak. (do not parse)*'],
  130. ['*Just some text. But it ends with newline (do not parse)\n*', '*Just some text. But it ends with newline (do not parse)\n*'],
  131. ]);
  132. });
  133. });
  134. describe('escapeHtml', function() {
  135. this.testPatterns = (cases) => testPatterns('escapeHtml', cases);
  136. it('escapes html tags', () => {
  137. this.testPatterns([
  138. ['<h1>heading</h1>', '&lt;h1&gt;heading&lt;/h1&gt;'],
  139. ['<b>< script >foo&ndash;</b>< script>', '&lt;b&gt;&lt; script &gt;foo&amp;ndash;&lt;/b&gt;&lt; script&gt;'],
  140. ['<a href="/">a</a>', '&lt;a href=&quot;/&quot;&gt;a&lt;/a&gt;'],
  141. ]);
  142. });
  143. });
  144. describe('mentionify', function() {
  145. this.testPatterns = (cases) => testPatterns('mentionify', cases);
  146. it('no mentions', () => {
  147. this.testPatterns([
  148. ['', ''],
  149. ['hello my friend', 'hello my friend'],
  150. ['@[AAAAAAA]', '@[AAAAAAA]'],
  151. ['this is not a valid @[AAAAAAA]', 'this is not a valid @[AAAAAAA]'],
  152. ['@[@@@@@@@]', '@[@@@@@@@]'],
  153. ['this is not a valid @[@@@@@@@]', 'this is not a valid @[@@@@@@@]'],
  154. ]);
  155. });
  156. it('mention - no contacts', () => {
  157. this.testPatterns([
  158. ['@[BBBBBBBB]', '@[BBBBBBBB]'],
  159. ['@[*BBBBBBB]', '@[*BBBBBBB]'],
  160. ]);
  161. });
  162. it('mention - contact', () => {
  163. this.testPatterns([
  164. ['@[AAAAAAAA]', '<span class="mention id AAAAAAAA" text="@[AAAAAAAA]">ContactA</span>'],
  165. ['hello @[AAAAAAAA]. @[AAAAAAAA] you are my friend', 'hello <span class="mention id AAAAAAAA" text="@[AAAAAAAA]">ContactA</span>. <span class="mention id AAAAAAAA" text="@[AAAAAAAA]">ContactA</span> you are my friend'],
  166. ['@[AAAAAAAA] @[AAAAAAAA] @[AAAAAAAA]', '<span class="mention id AAAAAAAA" text="@[AAAAAAAA]">ContactA</span> <span class="mention id AAAAAAAA" text="@[AAAAAAAA]">ContactA</span> <span class="mention id AAAAAAAA" text="@[AAAAAAAA]">ContactA</span>']
  167. ]);
  168. });
  169. it('mention - all', () => {
  170. this.testPatterns([
  171. ['@[@@@@@@@@]', '<span class="mention all" text="@[@@@@@@@@]">messenger.ALL</span>'],
  172. ['@[@@@@@@@@] your base are belong to us', '<span class="mention all" text="@[@@@@@@@@]">messenger.ALL</span> your base are belong to us'],
  173. ['@[@@@@@@@@] @[@@@@@@@@] @[@@@@@@@@]', '<span class="mention all" text="@[@@@@@@@@]">messenger.ALL</span> <span class="mention all" text="@[@@@@@@@@]">messenger.ALL</span> <span class="mention all" text="@[@@@@@@@@]">messenger.ALL</span>']
  174. ]);
  175. });
  176. it('mention - mixed', () => {
  177. this.testPatterns([
  178. ['@[@@@@@@@@] @[AAAAAAAA] @[BBBBBBBB]', '<span class="mention all" text="@[@@@@@@@@]">messenger.ALL</span> <span class="mention id AAAAAAAA" text="@[AAAAAAAA]">ContactA</span> @[BBBBBBBB]'],
  179. ]);
  180. });
  181. it('mention - me contact', () => {
  182. this.testPatterns([
  183. ['@[MEMEMEME]', '<span class="mention me" text="@[MEMEMEME]">Er</span>'],
  184. ['hello @[MEMEMEME]. @[MEMEMEME] you are my friend', 'hello <span class="mention me" text="@[MEMEMEME]">Er</span>. <span class="mention me" text="@[MEMEMEME]">Er</span> you are my friend'],
  185. ['@[MEMEMEME] @[MEMEMEME] @[MEMEMEME]', '<span class="mention me" text="@[MEMEMEME]">Er</span> <span class="mention me" text="@[MEMEMEME]">Er</span> <span class="mention me" text="@[MEMEMEME]">Er</span>']
  186. ]);
  187. });
  188. it('mention - escape html parameters', () => {
  189. this.testPatterns([
  190. ['@[BAD0BAD1]', '<span class="mention id BAD0BAD1" text="@[BAD0BAD1]">&lt;b&gt;&lt; script &gt;foo&amp;ndash;&lt;/b&gt;&lt; script&gt;</span>'],
  191. ]);
  192. });
  193. });
  194. describe('nlToBr', function() {
  195. this.testPatterns = (cases) => testPatterns('nlToBr', cases);
  196. it('converts newlines (enabled=true)', () => {
  197. const filter = $filter('nlToBr');
  198. expect(filter('abc \n def', true)).toEqual('abc <br> def');
  199. expect(filter('a\nb\nc\\n', true)).toEqual('a<br>b<br>c\\n');
  200. });
  201. it('does not converts newlines (enabled=false)', () => {
  202. const filter = $filter('nlToBr');
  203. expect(filter('abc\ndef', false)).toEqual('abc\ndef');
  204. });
  205. it('if enabled flag is not set, converts newlines', () => {
  206. const filter = $filter('nlToBr');
  207. expect(filter('abc\ndef')).toEqual('abc<br>def');
  208. });
  209. });
  210. describe('unixToTimestring', function() {
  211. this.testPatterns = (cases) => testPatterns('unixToTimestring', cases);
  212. it('shows only time for today', () => {
  213. const d1 = new Date(); d1.setHours(8); d1.setMinutes(7);
  214. const d2 = new Date(); d2.setHours(12); d2.setMinutes(14);
  215. const d3 = new Date(); d3.setHours(0); d3.setMinutes(0);
  216. this.testPatterns([
  217. [d1.getTime() / 1000, '08:07'],
  218. [d2.getTime() / 1000, '12:14'],
  219. [d3.getTime() / 1000, '00:00'],
  220. ]);
  221. });
  222. it('shows full date with forceFull flag', () => {
  223. const d = new Date();
  224. const formatted = $filter('unixToTimestring')(d.getTime() / 1000, true);
  225. expect(formatted.length > 10).toBe(true);
  226. expect(formatted).toContain(d.getFullYear().toString());
  227. });
  228. it('shows "yesterday" for yesterday', () => {
  229. const d1 = new Date();
  230. const ts = d1.getTime();
  231. const d2 = new Date(ts - 1000 * 60 * 60 * 24);
  232. d2.setHours(8); d2.setMinutes(7);
  233. this.testPatterns([
  234. [d2.getTime() / 1000, 'date.YESTERDAY, 08:07'],
  235. ]);
  236. });
  237. it('shows full datetime for other days', () => {
  238. jasmine.clock().install();
  239. jasmine.clock().mockDate(new Date(2018, 9, 9, 20, 42));
  240. const now = new Date();
  241. const d1 = new Date(2010, 1, 7, 18, 42);
  242. const d2 = new Date(now.getFullYear(), 4, 2, 23, 59);
  243. this.testPatterns([
  244. [d1.getTime() / 1000, '7. date.month_short.FEB 2010, 18:42'],
  245. [d2.getTime() / 1000, '2. date.month_short.MAY, 23:59'],
  246. ]);
  247. });
  248. });
  249. describe('dndModeSimplified', function() {
  250. let process = (dnd, sound) => {
  251. return $filter('dndModeSimplified')({
  252. notifications: {
  253. dnd: dnd,
  254. sound: sound,
  255. },
  256. })
  257. };
  258. it('dnd enabled', () => {
  259. expect(process(
  260. {mode: 'on'},
  261. {mode: 'default'}
  262. )).toEqual('on');
  263. });
  264. it('dnd enabled (no sound)', () => {
  265. expect(process(
  266. {mode: 'on'},
  267. {mode: 'muted'}
  268. )).toEqual('on');
  269. });
  270. it('dnd disabled', () => {
  271. expect(process(
  272. {mode: 'off'},
  273. {mode: 'default'}
  274. )).toEqual('off');
  275. });
  276. it('dnd disabled (no sound)', () => {
  277. expect(process(
  278. {mode: 'off'},
  279. {mode: 'muted'}
  280. )).toEqual('off');
  281. });
  282. it('mention only', () => {
  283. expect(process(
  284. {mode: 'on', mentionOnly: true},
  285. {mode: 'default'}
  286. )).toEqual('mention');
  287. });
  288. it('mention only (no sound)', () => {
  289. expect(process(
  290. {mode: 'on', mentionOnly: true},
  291. {mode: 'muted'}
  292. )).toEqual('mention');
  293. });
  294. it('until (not expired)', () => {
  295. jasmine.clock().install();
  296. jasmine.clock().mockDate(new Date(2018, 9, 9, 20, 42));
  297. expect(process(
  298. {mode: 'until', until: +(new Date(2018, 9, 9, 20, 50))},
  299. {mode: 'default'}
  300. )).toEqual('on');
  301. });
  302. it('until (expired)', () => {
  303. jasmine.clock().install();
  304. jasmine.clock().mockDate(new Date(2018, 9, 9, 20, 42));
  305. expect(process(
  306. {mode: 'until', until: +(new Date(2018, 9, 9, 19, 50))},
  307. {mode: 'default'}
  308. )).toEqual('off');
  309. });
  310. it('until (mention only, not expired)', () => {
  311. jasmine.clock().install();
  312. jasmine.clock().mockDate(new Date(2018, 9, 9, 20, 42));
  313. expect(process(
  314. {mode: 'until', until: +(new Date(2018, 9, 9, 20, 50)), mentionOnly: true},
  315. {mode: 'default'}
  316. )).toEqual('mention');
  317. });
  318. it('until (mention only, expired)', () => {
  319. jasmine.clock().install();
  320. jasmine.clock().mockDate(new Date(2018, 9, 9, 20, 42));
  321. expect(process(
  322. {mode: 'until', until: +(new Date(2018, 9, 9, 19, 50)), mentionOnly: true},
  323. {mode: 'default'}
  324. )).toEqual('off');
  325. });
  326. });
  327. describe('enlargeSingleEmoji', function() {
  328. let process = (text) => {
  329. return $filter('enlargeSingleEmoji')(text, true)
  330. };
  331. const singleEmojiClassName = 'large-emoji';
  332. const crazy = '<span class="e1 e1-people _1f92a" title=":crazy_face:">🤪</span>';
  333. const crazyLarge = '<span class="e1 ' + singleEmojiClassName + ' e1-people _1f92a" title=":crazy_face:">🤪</span>';
  334. const copyright = '<span class="e1 e1-symbols _00a9" title=":copyright:">©️</span>';
  335. const copyrightLarge = '<span class="e1 ' + singleEmojiClassName + ' e1-symbols _00a9" title=":copyright:">©️</span>';
  336. it('enlarges 1 emoji', () => {
  337. expect(process(crazy)).toEqual(crazyLarge);
  338. });
  339. it('enlarges 2 emoji', () => {
  340. expect(process(crazy + copyright)).toEqual(crazyLarge + copyrightLarge);
  341. });
  342. it('enlarges 3 emoji', () => {
  343. expect(process(crazy + copyright + crazy)).toEqual(crazyLarge + copyrightLarge + crazyLarge);
  344. });
  345. it('does not enlarge 4 emoji', () => {
  346. expect(process(crazy + copyright + crazy + copyright)).toEqual(crazy + copyright + crazy + copyright);
  347. });
  348. it('does not enlarge if non-emoji characters are contained', () => {
  349. expect(process(crazy + ' ')).toEqual(crazy + ' ');
  350. expect(process(crazy + 'a' + crazy)).toEqual(crazy + 'a' + crazy);
  351. });
  352. it('does not modify non emoji text', () => {
  353. const text = 'emoji e1 e1-people hello';
  354. expect(process(text)).toEqual(text);
  355. });
  356. it('does nothing if enlarge flag is set to false', () => {
  357. expect($filter('enlargeSingleEmoji')(crazy, false)).toEqual(crazy);
  358. });
  359. });
  360. describe('linkify', function() {
  361. let process = (text) => {
  362. return $filter('linkify')(text)
  363. };
  364. it('links http urls', () => {
  365. expect(process('hello https://threema.ch/!'))
  366. .toEqual('hello <a href="https://threema.ch/" class="autolinked autolinked-url" target="_blank" rel="noopener noreferrer">https://threema.ch</a>!');
  367. });
  368. it('links e-mails', () => {
  369. expect(process('hello info@threema.ch!'))
  370. .toEqual('hello <a href="mailto:info@threema.ch" class="autolinked autolinked-email" target="_blank" rel="noopener noreferrer">info@threema.ch</a>!');
  371. });
  372. it('does not link phone numbers', () => {
  373. const input = 'hello +41791234567';
  374. expect(process(input)).toEqual(input);
  375. });
  376. it('does not link mentions', () => {
  377. const input = 'hello @threemaapp';
  378. expect(process(input)).toEqual(input);
  379. });
  380. it('does not link hashtags', () => {
  381. const input = 'hello #threema';
  382. expect(process(input)).toEqual(input);
  383. });
  384. });
  385. });