Alert.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. 'use strict';
  2. const common = require('../common');
  3. const fc = require('fast-check');
  4. const path = require('path');
  5. /**
  6. * icon markup of the bootstrap5 template
  7. *
  8. * @param {string} name - bootstrap icon name
  9. * @returns {string}
  10. */
  11. function icon(name) {
  12. return '<svg width="16" height="16" fill="currentColor" aria-hidden="true">' +
  13. '<use href="img/bootstrap-icons.svg#' + name + '"></use></svg>';
  14. }
  15. describe('Alert', function () {
  16. describe('showStatus', function () {
  17. it('shows a status message (basic)', () => {
  18. fc.assert(fc.property(
  19. fc.array(common.fcAlnumString()),
  20. fc.array(common.fcAlnumString()),
  21. function (icon, message) {
  22. icon = icon.join('');
  23. message = message.join('');
  24. const expected = '<div id="status">' + message + '</div>';
  25. document.body.innerHTML =
  26. '<div id="status"></div>';
  27. PrivateBin.Alert.init();
  28. PrivateBin.Alert.showStatus(message, icon);
  29. const result = document.body.innerHTML;
  30. return expected === result;
  31. }
  32. ));
  33. });
  34. it('shows a status message (bootstrap)', () => {
  35. fc.assert(fc.property(
  36. fc.array(common.fcAlnumString()),
  37. function (message) {
  38. message = message.join('');
  39. const expected = '<div id="status" role="alert" ' +
  40. 'class="d-flex align-items-center gap-2 alert alert-info">' +
  41. '<div>' + icon('info-circle') + ' <span>' +
  42. message + '</span></div></div>';
  43. document.body.innerHTML =
  44. '<div id="status" role="alert" class="d-flex ' +
  45. 'align-items-center gap-2 alert alert-info hidden">' +
  46. '<div>' + icon('info-circle') + ' </div></div>';
  47. PrivateBin.Alert.init();
  48. PrivateBin.Alert.showStatus(message);
  49. const result = document.body.innerHTML;
  50. return expected === result;
  51. }
  52. ));
  53. });
  54. it('shows a status message (bootstrap, custom icon)', () => {
  55. fc.assert(fc.property(
  56. fc.array(common.fcAlnumString()),
  57. fc.array(common.fcAlnumString()),
  58. function (customIcon, message) {
  59. customIcon = customIcon.join('');
  60. message = message.join('');
  61. const expected = '<div id="status" role="alert" ' +
  62. 'class="d-flex align-items-center gap-2 alert alert-info">' +
  63. '<div>' + icon(customIcon || 'info-circle') + ' <span>' +
  64. message + '</span></div></div>';
  65. document.body.innerHTML =
  66. '<div id="status" role="alert" class="d-flex ' +
  67. 'align-items-center gap-2 alert alert-info hidden">' +
  68. '<div>' + icon('info-circle') + ' </div></div>';
  69. PrivateBin.Alert.init();
  70. PrivateBin.Alert.showStatus(message, customIcon);
  71. const result = document.body.innerHTML;
  72. return expected === result;
  73. }
  74. ));
  75. });
  76. it('replaces a status message rendered by the server, keeping its buttons (bootstrap)', () => {
  77. fc.assert(fc.property(
  78. fc.array(common.fcAlnumString(), {minLength: 1}),
  79. fc.array(common.fcAlnumString(), {minLength: 1}),
  80. function (serverMessage, message) {
  81. serverMessage = 'server ' + serverMessage.join('');
  82. message = message.join('');
  83. const button = '<button type="button" id="new-from-alert">Start over</button>';
  84. document.body.innerHTML =
  85. '<div id="status" role="alert" class="d-flex ' +
  86. 'align-items-center gap-2 alert alert-success"><div>' +
  87. icon('info-circle') + ' ' + serverMessage + ' </div>' +
  88. button + '</div>';
  89. PrivateBin.Alert.init();
  90. PrivateBin.Alert.showStatus(message);
  91. const status = document.getElementById('status');
  92. return status.innerHTML === '<div>' + icon('info-circle') +
  93. ' <span>' + message + '</span></div>' + button &&
  94. status.textContent.indexOf(serverMessage) === -1;
  95. }
  96. ));
  97. });
  98. });
  99. describe('showWarning', function () {
  100. it('shows a warning message (basic)', () => {
  101. fc.assert(fc.property(
  102. fc.array(common.fcAlnumString()),
  103. fc.array(common.fcAlnumString()),
  104. function (icon, message) {
  105. icon = icon.join('');
  106. message = message.join('');
  107. const expected = '<div id="errormessage">' + message + '</div>';
  108. document.body.innerHTML =
  109. '<div id="errormessage"></div>';
  110. PrivateBin.Alert.init();
  111. PrivateBin.Alert.showWarning(message, icon);
  112. const result = document.body.innerHTML;
  113. return expected === result;
  114. }
  115. ));
  116. });
  117. it('shows a warning message (bootstrap)', () => {
  118. fc.assert(fc.property(
  119. fc.array(common.fcAlnumString()),
  120. function (message) {
  121. message = message.join('');
  122. const expected = '<div id="errormessage" role="alert" ' +
  123. 'class="alert alert-danger">' + icon('exclamation-triangle') +
  124. ' <span>' + message + '</span></div>';
  125. document.body.innerHTML =
  126. '<div id="errormessage" role="alert" class="hidden ' +
  127. 'alert alert-danger">' + icon('exclamation-triangle') + ' </div>';
  128. PrivateBin.Alert.init();
  129. PrivateBin.Alert.showWarning(message);
  130. const result = document.body.innerHTML;
  131. return expected === result;
  132. }
  133. ));
  134. });
  135. it('shows a warning message (bootstrap, custom icon)', () => {
  136. fc.assert(fc.property(
  137. fc.array(common.fcAlnumString()),
  138. fc.array(common.fcAlnumString()),
  139. function (customIcon, message) {
  140. customIcon = customIcon.join('');
  141. message = message.join('');
  142. const expected = '<div id="errormessage" role="alert" ' +
  143. 'class="alert alert-danger">' +
  144. icon(customIcon || 'exclamation-triangle') +
  145. ' <span>' + message + '</span></div>';
  146. document.body.innerHTML =
  147. '<div id="errormessage" role="alert" class="hidden ' +
  148. 'alert alert-danger">' + icon('exclamation-triangle') + ' </div>';
  149. PrivateBin.Alert.init();
  150. PrivateBin.Alert.showWarning(message, customIcon);
  151. const result = document.body.innerHTML;
  152. return expected === result;
  153. }
  154. ));
  155. });
  156. });
  157. describe('showError', function () {
  158. it('shows an error message (basic)', () => {
  159. fc.assert(fc.property(
  160. fc.array(common.fcAlnumString()),
  161. fc.array(common.fcAlnumString()),
  162. function (icon, message) {
  163. icon = icon.join('');
  164. message = message.join('');
  165. const expected = '<div id="errormessage">' + message + '</div>';
  166. document.body.innerHTML =
  167. '<div id="errormessage"></div>';
  168. PrivateBin.Alert.init();
  169. PrivateBin.Alert.showError(message, icon);
  170. const result = document.body.innerHTML;
  171. return expected === result;
  172. }
  173. ));
  174. });
  175. it('shows an error message (bootstrap)', () => {
  176. fc.assert(fc.property(
  177. fc.array(common.fcAlnumString()),
  178. function (message) {
  179. message = message.join('');
  180. const expected = '<div id="errormessage" role="alert" ' +
  181. 'class="alert alert-danger">' + icon('exclamation-triangle') +
  182. ' <span>' + message + '</span></div>';
  183. document.body.innerHTML =
  184. '<div id="errormessage" role="alert" class="hidden ' +
  185. 'alert alert-danger">' + icon('exclamation-triangle') + ' </div>';
  186. PrivateBin.Alert.init();
  187. PrivateBin.Alert.showError(message);
  188. const result = document.body.innerHTML;
  189. return expected === result;
  190. }
  191. ));
  192. });
  193. it('shows an error message (bootstrap, custom icon)', () => {
  194. fc.assert(fc.property(
  195. fc.array(common.fcAlnumString()),
  196. fc.array(common.fcAlnumString()),
  197. function (customIcon, message) {
  198. customIcon = customIcon.join('');
  199. message = message.join('');
  200. const expected = '<div id="errormessage" role="alert" ' +
  201. 'class="alert alert-danger">' +
  202. icon(customIcon || 'exclamation-triangle') +
  203. ' <span>' + message + '</span></div>';
  204. document.body.innerHTML =
  205. '<div id="errormessage" role="alert" class="hidden ' +
  206. 'alert alert-danger">' + icon('exclamation-triangle') + ' </div>';
  207. PrivateBin.Alert.init();
  208. PrivateBin.Alert.showError(message, customIcon);
  209. const result = document.body.innerHTML;
  210. return expected === result;
  211. }
  212. ));
  213. });
  214. });
  215. describe('showRemaining', function () {
  216. it('shows remaining time (basic)', () => {
  217. fc.assert(fc.property(
  218. fc.array(common.fcAlnumString()),
  219. fc.array(common.fcAlnumString()),
  220. fc.integer(),
  221. function (message, string, number) {
  222. message = message.join('');
  223. string = string.join('');
  224. const expected = '<div id="remainingtime" class="">' + string + message + number + '</div>';
  225. document.body.innerHTML =
  226. '<div id="remainingtime" class="hidden"></div>';
  227. PrivateBin.Alert.init();
  228. PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]);
  229. const result = document.body.innerHTML;
  230. return expected === result;
  231. }
  232. ));
  233. });
  234. it('shows remaining time (bootstrap)', () => {
  235. fc.assert(fc.property(
  236. fc.array(common.fcAlnumString()),
  237. fc.array(common.fcAlnumString()),
  238. fc.integer(),
  239. function (message, string, number) {
  240. message = message.join('');
  241. string = string.join('');
  242. const expected = '<div id="remainingtime" role="alert" ' +
  243. 'class="alert alert-info">' + icon('fire') +
  244. ' <span>' + string + message + number + '</span></div>';
  245. document.body.innerHTML =
  246. '<div id="remainingtime" role="alert" class="hidden ' +
  247. 'alert alert-info">' + icon('fire') + ' </div>';
  248. PrivateBin.Alert.init();
  249. PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]);
  250. const result = document.body.innerHTML;
  251. return expected === result;
  252. }
  253. ));
  254. });
  255. });
  256. describe('showLoading', function () {
  257. it('shows a loading message (basic)', () => {
  258. fc.assert(fc.property(
  259. fc.array(common.fcAlnumString()),
  260. fc.array(common.fcAlnumString()),
  261. function (message, icon) {
  262. message = message.join('');
  263. icon = icon.join('');
  264. const defaultMessage = 'Loading…';
  265. if (message.length === 0) {
  266. message = defaultMessage;
  267. }
  268. const expected = '<div id="loadingindicator" class="">' + message + '</div>';
  269. document.body.innerHTML =
  270. '<div id="loadingindicator" class="hidden">' + defaultMessage + '</div>';
  271. PrivateBin.Alert.init();
  272. PrivateBin.Alert.showLoading(message, icon);
  273. const result = document.body.innerHTML;
  274. return expected === result;
  275. }
  276. ));
  277. });
  278. it('shows a loading message (bootstrap)', () => {
  279. fc.assert(fc.property(
  280. fc.array(common.fcAlnumString()),
  281. fc.array(common.fcAlnumString()),
  282. function (message, customIcon) {
  283. message = message.join('');
  284. customIcon = customIcon.join('');
  285. const defaultMessage = 'Loading…';
  286. if (message.length === 0) {
  287. message = defaultMessage;
  288. }
  289. const expected = '<ul class="navbar-nav"><li ' +
  290. 'id="loadingindicator" class="navbar-text me-auto">' +
  291. icon(customIcon || 'clock') + ' <span>' + message +
  292. '</span></li></ul>';
  293. document.body.innerHTML =
  294. '<ul class="navbar-nav"><li id="loadingindicator" ' +
  295. 'class="navbar-text hidden me-auto">' + icon('clock') +
  296. ' ' + defaultMessage + '</li></ul>';
  297. PrivateBin.Alert.init();
  298. PrivateBin.Alert.showLoading(message, customIcon);
  299. const result = document.body.innerHTML;
  300. return expected === result;
  301. }
  302. ));
  303. });
  304. it('keeps the previous icon, if none is given (bootstrap)', () => {
  305. fc.assert(fc.property(
  306. fc.constantFrom('clock', 'cloud-download', 'cloud-upload'),
  307. fc.constantFrom(undefined, null, ''),
  308. function (customIcon, noIcon) {
  309. document.body.innerHTML =
  310. '<ul class="navbar-nav"><li id="loadingindicator" ' +
  311. 'class="navbar-text hidden me-auto">' + icon('clock') +
  312. ' Loading…</li></ul>';
  313. PrivateBin.Alert.init();
  314. PrivateBin.Alert.showLoading('first', customIcon);
  315. PrivateBin.Alert.hideLoading();
  316. PrivateBin.Alert.showLoading('second', noIcon);
  317. const use = document.querySelector('#loadingindicator use');
  318. return use.getAttribute('href') === 'img/bootstrap-icons.svg#' + customIcon &&
  319. document.querySelectorAll('#loadingindicator svg').length === 1 &&
  320. document.querySelector('#loadingindicator span').textContent === 'second';
  321. }
  322. ));
  323. });
  324. });
  325. describe('hideLoading', function () {
  326. it(
  327. 'hides the loading message',
  328. function() {
  329. document.body.innerHTML =
  330. '<ul class="navbar-nav"><li id="loadingindicator" ' +
  331. 'class="navbar-text me-auto">' + icon('clock') +
  332. ' Loading…</li></ul>';
  333. document.body.classList.add('loading');
  334. PrivateBin.Alert.init();
  335. PrivateBin.Alert.hideLoading();
  336. assert.ok(!document.body.classList.contains('loading'));
  337. assert.ok(document.getElementById('loadingindicator').classList.contains('hidden'));
  338. }
  339. );
  340. });
  341. describe('hideMessages', function () {
  342. it(
  343. 'hides all messages',
  344. function() {
  345. document.body.innerHTML =
  346. '<div id="status" role="alert" class="d-flex ' +
  347. 'align-items-center gap-2 alert alert-info"><div>' +
  348. icon('info-circle') + ' </div></div>' +
  349. '<div id="errormessage" role="alert" class="alert ' +
  350. 'alert-danger">' + icon('exclamation-triangle') + ' </div>';
  351. PrivateBin.Alert.init();
  352. PrivateBin.Alert.hideMessages();
  353. assert.ok(document.getElementById('status').classList.contains('hidden'));
  354. assert.ok(document.getElementById('errormessage').classList.contains('hidden'));
  355. }
  356. );
  357. });
  358. describe('setCustomHandler', function () {
  359. it('calls a given handler function', () => {
  360. fc.assert(fc.property(
  361. fc.integer({min: 0, max: 3}),
  362. fc.array(common.fcAlnumString()),
  363. function (trigger, message) {
  364. message = message.join('');
  365. let handlerCalled = false,
  366. defaultMessage = 'Loading…',
  367. functions = [
  368. PrivateBin.Alert.showStatus,
  369. PrivateBin.Alert.showError,
  370. PrivateBin.Alert.showRemaining,
  371. PrivateBin.Alert.showLoading
  372. ];
  373. if (message.length === 0) {
  374. message = defaultMessage;
  375. }
  376. document.body.innerHTML =
  377. '<ul class="navbar-nav"><li id="loadingindicator" ' +
  378. 'class="navbar-text hidden me-auto">' + icon('clock') +
  379. ' ' + defaultMessage + '</li></ul>' +
  380. '<div id="remainingtime" role="alert" class="hidden ' +
  381. 'alert alert-info">' + icon('fire') + ' </div>' +
  382. '<div id="status" role="alert" class="d-flex ' +
  383. 'align-items-center gap-2 alert alert-info"><div>' +
  384. icon('info-circle') + ' </div></div>' +
  385. '<div id="errormessage" role="alert" class="alert ' +
  386. 'alert-danger">' + icon('exclamation-triangle') + ' </div>';
  387. PrivateBin.Alert.init();
  388. PrivateBin.Alert.setCustomHandler(function(id, element) {
  389. handlerCalled = true;
  390. return Math.random() > 0.5 ? true : element;
  391. });
  392. functions[trigger](message);
  393. PrivateBin.Alert.setCustomHandler(null);
  394. return handlerCalled;
  395. }
  396. ));
  397. });
  398. });
  399. describe('icons', function () {
  400. it('only uses icons that exist in the icon sprite', function () {
  401. const source = fs.readFileSync(path.join(__dirname, '..', 'privatebin.js'), 'utf8');
  402. const sprite = fs.readFileSync(path.join(__dirname, '..', '..', 'img', 'bootstrap-icons.svg'), 'utf8');
  403. const calls = source.match(/(Alert\.show(Status|Warning|Error|Loading)|Helper\.setIcon)\([^;]*\);/g);
  404. assert.ok(calls.length > 0, 'found icon setting calls');
  405. let names = [];
  406. calls.forEach(function (call) {
  407. // skip the first argument, the message or element
  408. const icons = call.substring(call.indexOf(',')).match(/'[a-z][a-z0-9-]*'/g) || [];
  409. names = names.concat(icons.map(name => name.slice(1, -1)));
  410. });
  411. assert.ok(names.length > 0, 'found icon names');
  412. names.forEach(function (name) {
  413. assert.ok(sprite.includes('id="' + name + '"'), 'icon ' + name + ' exists in sprite');
  414. });
  415. });
  416. });
  417. });