TopNav.js 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. 'use strict';
  2. require('../common');
  3. const fc = require('fast-check');
  4. function query(selector) {
  5. if (selector.startsWith('#')) {
  6. return document.getElementById(selector.slice(1));
  7. }
  8. return document.querySelector(selector);
  9. }
  10. describe('TopNav', function () {
  11. describe('hideBurnAfterReadingButtons', function () {
  12. it(
  13. 'hides buttons when the QR code button is disabled',
  14. function () {
  15. document.documentElement.innerHTML =
  16. '<nav><button id="clonebutton"></button></nav>';
  17. PrivateBin.TopNav.init();
  18. assert.doesNotThrow(function () {
  19. PrivateBin.TopNav.hideBurnAfterReadingButtons();
  20. });
  21. assert.ok(query('#clonebutton').classList.contains('hidden'));
  22. cleanup();
  23. }
  24. );
  25. });
  26. describe('showViewButtons & hideViewButtons', function () {
  27. before(function () {
  28. cleanup();
  29. });
  30. it(
  31. 'shows and hides navigation elements when the QR code button is disabled',
  32. function () {
  33. document.documentElement.innerHTML =
  34. '<nav><button id="newbutton" class="hidden"></button>' +
  35. '<button id="clonebutton" class="hidden"></button>' +
  36. '<button id="rawtextbutton" class="hidden"></button>' +
  37. '<button id="downloadtextbutton" class="hidden"></button></nav>';
  38. PrivateBin.TopNav.init();
  39. assert.doesNotThrow(function () {
  40. PrivateBin.TopNav.showViewButtons();
  41. });
  42. assert.ok(!query('#newbutton').classList.contains('hidden'));
  43. assert.ok(!query('#clonebutton').classList.contains('hidden'));
  44. assert.ok(!query('#rawtextbutton').classList.contains('hidden'));
  45. assert.ok(!query('#downloadtextbutton').classList.contains('hidden'));
  46. assert.doesNotThrow(function () {
  47. PrivateBin.TopNav.hideViewButtons();
  48. });
  49. assert.ok(query('#newbutton').classList.contains('hidden'));
  50. assert.ok(query('#clonebutton').classList.contains('hidden'));
  51. assert.ok(query('#rawtextbutton').classList.contains('hidden'));
  52. assert.ok(query('#downloadtextbutton').classList.contains('hidden'));
  53. cleanup();
  54. }
  55. );
  56. it(
  57. 'displays & hides navigation elements for viewing an existing document',
  58. function () {
  59. let results = [];
  60. document.documentElement.innerHTML =
  61. `<nav class="navbar navbar-inverse navbar-static-top">
  62. <div id="navbar" class="navbar-collapse collapse">
  63. <ul class="nav navbar-nav"><li><button id="newbutton" type="button" class="hidden btn btn-warning navbar-btn">
  64. <span class="glyphicon glyphicon-file" aria-hidden="true">
  65. </span> New</button><button id="clonebutton" type="button" class="hidden btn btn-warning navbar-btn">
  66. <span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> Clone</button>
  67. <button id="rawtextbutton" type="button" class="hidden btn btn-warning navbar-btn">
  68. <span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> Raw text</button>
  69. <button id="downloadtextbutton" type="button" class="hidden btn btn-default navbar-btn"></button>
  70. <button id="qrcodelink" type="button" data-toggle="modal" data-target="#qrcodemodal" class="hidden btn btn-warning navbar-btn"/>
  71. <span class="glyphicon glyphicon-qrcode" aria-hidden="true"></span> QR code</button></li></ul></div>
  72. </nav>`;
  73. PrivateBin.TopNav.init();
  74. results.push(
  75. query('#newbutton').classList.contains('hidden') &&
  76. query('#clonebutton').classList.contains('hidden') &&
  77. query('#rawtextbutton').classList.contains('hidden') &&
  78. query('#qrcodelink').classList.contains('hidden')
  79. );
  80. PrivateBin.TopNav.showViewButtons();
  81. results.push(
  82. !query('#newbutton').classList.contains('hidden') &&
  83. !query('#clonebutton').classList.contains('hidden') &&
  84. !query('#rawtextbutton').classList.contains('hidden') &&
  85. !query('#qrcodelink').classList.contains('hidden')
  86. );
  87. PrivateBin.TopNav.hideViewButtons();
  88. results.push(
  89. query('#newbutton').classList.contains('hidden') &&
  90. query('#clonebutton').classList.contains('hidden') &&
  91. query('#rawtextbutton').classList.contains('hidden') &&
  92. query('#qrcodelink').classList.contains('hidden')
  93. );
  94. cleanup();
  95. const result = results.every(element => element);
  96. if (!result) {
  97. console.log(results);
  98. }
  99. assert.ok(result);
  100. }
  101. );
  102. });
  103. describe('showCreateButtons & hideCreateButtons', function () {
  104. before(function () {
  105. cleanup();
  106. });
  107. it(
  108. 'displays & hides navigation elements for creating a document',
  109. function () { // eslint-disable-line complexity
  110. let results = [];
  111. document.documentElement.innerHTML =
  112. '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
  113. 'type="button" class="hidden">New</button></li><li><a ' +
  114. 'id="expiration" href="#" class="hidden">Expiration</a>' +
  115. '</li><li><div id="burnafterreadingoption" class="hidden">' +
  116. 'Burn after reading</div></li><li><div id="opendiscussion' +
  117. 'option" class="hidden">Open discussion</div></li><li>' +
  118. '<div id="password" class="hidden">Password</div></li>' +
  119. '<li id="attach" class="hidden">Attach a file</li><li>' +
  120. '<a id="formatter" href="#" class="hidden">Format</a>' +
  121. '</li><li><button id="sendbutton" type="button" ' +
  122. 'class="hidden">Create</button></li></ul></div></nav>';
  123. PrivateBin.TopNav.init();
  124. results.push(
  125. query('#sendbutton').classList.contains('hidden') &&
  126. query('#expiration').classList.contains('hidden') &&
  127. query('#formatter').classList.contains('hidden') &&
  128. query('#burnafterreadingoption').classList.contains('hidden') &&
  129. query('#opendiscussionoption').classList.contains('hidden') &&
  130. query('#newbutton').classList.contains('hidden') &&
  131. query('#password').classList.contains('hidden') &&
  132. query('#attach').classList.contains('hidden')
  133. );
  134. PrivateBin.TopNav.showCreateButtons();
  135. results.push(
  136. !query('#sendbutton').classList.contains('hidden') &&
  137. !query('#expiration').classList.contains('hidden') &&
  138. !query('#formatter').classList.contains('hidden') &&
  139. !query('#burnafterreadingoption').classList.contains('hidden') &&
  140. !query('#opendiscussionoption').classList.contains('hidden') &&
  141. !query('#newbutton').classList.contains('hidden') &&
  142. !query('#password').classList.contains('hidden') &&
  143. !query('#attach').classList.contains('hidden')
  144. );
  145. PrivateBin.TopNav.hideCreateButtons();
  146. results.push(
  147. query('#sendbutton').classList.contains('hidden') &&
  148. query('#expiration').classList.contains('hidden') &&
  149. query('#formatter').classList.contains('hidden') &&
  150. query('#burnafterreadingoption').classList.contains('hidden') &&
  151. query('#opendiscussionoption').classList.contains('hidden') &&
  152. query('#newbutton').classList.contains('hidden') &&
  153. query('#password').classList.contains('hidden') &&
  154. query('#attach').classList.contains('hidden')
  155. );
  156. cleanup();
  157. const result = results.every(element => element);
  158. if (!result) {
  159. console.log(results);
  160. }
  161. assert.ok(result);
  162. }
  163. );
  164. it(
  165. 'tolerates missing optional elements (discussion & password disabled)',
  166. function () {
  167. // with discussion = false and password = false in the
  168. // configuration, the template omits these elements entirely
  169. document.documentElement.innerHTML =
  170. '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
  171. 'type="button" class="hidden">New</button></li><li><a ' +
  172. 'id="expiration" href="#" class="hidden">Expiration</a>' +
  173. '</li><li><div id="burnafterreadingoption" class="hidden">' +
  174. 'Burn after reading</div></li><li>' +
  175. '<a id="formatter" href="#" class="hidden">Format</a>' +
  176. '</li><li><button id="sendbutton" type="button" ' +
  177. 'class="hidden">Create</button></li></ul></div></nav>';
  178. PrivateBin.TopNav.init();
  179. assert.doesNotThrow(() => PrivateBin.TopNav.showCreateButtons());
  180. assert.ok(!query('#sendbutton').classList.contains('hidden'));
  181. assert.ok(!query('#burnafterreadingoption').classList.contains('hidden'));
  182. assert.doesNotThrow(() => PrivateBin.TopNav.hideCreateButtons());
  183. assert.ok(query('#sendbutton').classList.contains('hidden'));
  184. assert.ok(query('#burnafterreadingoption').classList.contains('hidden'));
  185. cleanup();
  186. }
  187. );
  188. });
  189. describe('send shortcut', function () {
  190. let sendCount;
  191. // builds the create-document navbar and stubs the actual send, so no
  192. // server interaction happens
  193. function setUpCreateMode() {
  194. cleanup();
  195. sendCount = 0;
  196. document.documentElement.innerHTML =
  197. '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
  198. 'type="button" class="hidden">New</button></li><li><a ' +
  199. 'id="expiration" href="#" class="hidden">Expiration</a>' +
  200. '</li><li><div id="burnafterreadingoption" class="hidden">' +
  201. 'Burn after reading</div></li><li><div id="opendiscussion' +
  202. 'option" class="hidden">Open discussion</div></li><li>' +
  203. '<div id="password" class="hidden">Password</div></li>' +
  204. '<li id="attach" class="hidden">Attach a file</li><li>' +
  205. '<a id="formatter" href="#" class="hidden">Format</a>' +
  206. '</li><li><button id="sendbutton" type="button" ' +
  207. 'class="hidden">Create</button></li></ul></div></nav>';
  208. PrivateBin.PasteEncrypter.sendPaste = function () {
  209. sendCount++;
  210. };
  211. PrivateBin.TopNav.init();
  212. PrivateBin.TopNav.showCreateButtons();
  213. }
  214. // dispatches a keydown on the document, returns whether the default was prevented
  215. function pressKey(options) {
  216. const event = new window.KeyboardEvent('keydown', Object.assign(
  217. {bubbles: true, cancelable: true}, options
  218. ));
  219. document.dispatchEvent(event);
  220. return event.defaultPrevented;
  221. }
  222. afterEach(function () {
  223. cleanup();
  224. });
  225. it('sends the document on Ctrl+S and suppresses the browser save dialog', function () {
  226. setUpCreateMode();
  227. const prevented = pressKey({key: 's', code: 'KeyS', ctrlKey: true});
  228. assert.strictEqual(sendCount, 1);
  229. assert.ok(prevented);
  230. });
  231. it('sends the document on Cmd+S (macOS)', function () {
  232. setUpCreateMode();
  233. const prevented = pressKey({key: 's', code: 'KeyS', metaKey: true});
  234. assert.strictEqual(sendCount, 1);
  235. assert.ok(prevented);
  236. });
  237. it('sends on the physical S key of non-Latin keyboard layouts', function () {
  238. setUpCreateMode();
  239. // on a Russian layout the S key produces "ы"
  240. pressKey({key: 'ы', code: 'KeyS', ctrlKey: true});
  241. assert.strictEqual(sendCount, 1);
  242. });
  243. it('leaves Ctrl+S to the browser when viewing a document', function () {
  244. setUpCreateMode();
  245. PrivateBin.TopNav.hideCreateButtons();
  246. const prevented = pressKey({key: 's', code: 'KeyS', ctrlKey: true});
  247. assert.strictEqual(sendCount, 0);
  248. assert.ok(!prevented);
  249. });
  250. it('ignores S without Ctrl/Cmd, with AltGr (Ctrl+Alt), with Shift, or other keys', function () {
  251. setUpCreateMode();
  252. const prevented = [
  253. pressKey({key: 's', code: 'KeyS'}),
  254. pressKey({key: 's', code: 'KeyS', ctrlKey: true, altKey: true}),
  255. pressKey({key: 'S', code: 'KeyS', ctrlKey: true, shiftKey: true}),
  256. pressKey({key: 'd', code: 'KeyD', ctrlKey: true})
  257. ];
  258. assert.strictEqual(sendCount, 0);
  259. assert.deepStrictEqual(prevented, [false, false, false, false]);
  260. });
  261. });
  262. describe('showNewPasteButton', function () {
  263. before(function () {
  264. cleanup();
  265. });
  266. it(
  267. 'displays the button for creating a document',
  268. function () {
  269. let results = [];
  270. document.documentElement.innerHTML =
  271. '<nav><div id="navbar"><ul><li><button id="newbutton" type=' +
  272. '"button" class="hidden">New</button></li></ul></div></nav>';
  273. PrivateBin.TopNav.init();
  274. results.push(
  275. query('#newbutton').classList.contains('hidden')
  276. );
  277. PrivateBin.TopNav.showNewPasteButton();
  278. results.push(
  279. !query('#newbutton').classList.contains('hidden')
  280. );
  281. cleanup();
  282. const result = results.every(element => element);
  283. if (!result) {
  284. console.log(results);
  285. }
  286. assert.ok(result);
  287. }
  288. );
  289. });
  290. describe('hideCloneButton', function () {
  291. before(function () {
  292. cleanup();
  293. });
  294. it(
  295. 'hides the button for cloning a document',
  296. function () {
  297. let results = [];
  298. document.documentElement.innerHTML =
  299. '<nav><div id="navbar"><ul><li><button id="clonebutton" ' +
  300. 'type="button" class="btn btn-warning navbar-btn">' +
  301. '<span class="glyphicon glyphicon-duplicate" aria-hidden=' +
  302. '"true"></span> Clone</button></li></ul></div></nav>';
  303. PrivateBin.TopNav.init();
  304. results.push(
  305. !query('#clonebutton').classList.contains('hidden')
  306. );
  307. PrivateBin.TopNav.hideCloneButton();
  308. results.push(
  309. query('#clonebutton').classList.contains('hidden')
  310. );
  311. cleanup();
  312. const result = results.every(element => element);
  313. if (!result) {
  314. console.log(results);
  315. }
  316. assert.ok(result);
  317. }
  318. );
  319. });
  320. describe('hideRawButton', function () {
  321. before(function () {
  322. cleanup();
  323. });
  324. it(
  325. 'hides the raw text button',
  326. function () {
  327. let results = [];
  328. document.documentElement.innerHTML =
  329. '<nav><div id="navbar"><ul><li><button ' +
  330. 'id="rawtextbutton" type="button" class="btn ' +
  331. 'btn-warning navbar-btn"><span class="glyphicon ' +
  332. 'glyphicon-text-background" aria-hidden="true"></span> ' +
  333. 'Raw text</button></li></ul></div></nav>';
  334. PrivateBin.TopNav.init();
  335. results.push(
  336. !query('#rawtextbutton').classList.contains('hidden')
  337. );
  338. PrivateBin.TopNav.hideRawButton();
  339. results.push(
  340. query('#rawtextbutton').classList.contains('hidden')
  341. );
  342. cleanup();
  343. const result = results.every(element => element);
  344. if (!result) {
  345. console.log(results);
  346. }
  347. assert.ok(result);
  348. }
  349. );
  350. });
  351. describe('hideFileSelector', function () {
  352. before(function () {
  353. cleanup();
  354. });
  355. it(
  356. 'hides the file attachment selection button',
  357. function () {
  358. let results = [];
  359. document.documentElement.innerHTML =
  360. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  361. 'dropdown"><a href="#" class="dropdown-toggle" data-' +
  362. 'toggle="dropdown" role="button" aria-haspopup="true" ' +
  363. 'aria-expanded="false">Attach a file <span class="caret">' +
  364. '</span></a><ul class="dropdown-menu"><li id="filewrap">' +
  365. '<div><input type="file" id="file" name="file" /></div>' +
  366. '</li><li id="customattachment" class="hidden"></li><li>' +
  367. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  368. '</li></ul></li></ul></div></nav>';
  369. PrivateBin.TopNav.init();
  370. results.push(
  371. !query('#filewrap').classList.contains('hidden')
  372. );
  373. PrivateBin.TopNav.hideFileSelector();
  374. results.push(
  375. query('#filewrap').classList.contains('hidden')
  376. );
  377. cleanup();
  378. const result = results.every(element => element);
  379. if (!result) {
  380. console.log(results);
  381. }
  382. assert.ok(result);
  383. }
  384. );
  385. });
  386. describe('showCustomAttachment', function () {
  387. before(function () {
  388. cleanup();
  389. });
  390. it(
  391. 'display the custom file attachment',
  392. function () {
  393. let results = [];
  394. document.documentElement.innerHTML =
  395. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  396. 'dropdown"><a href="#" class="dropdown-toggle" data-' +
  397. 'toggle="dropdown" role="button" aria-haspopup="true" ' +
  398. 'aria-expanded="false">Attach a file <span class="caret">' +
  399. '</span></a><ul class="dropdown-menu"><li id="filewrap">' +
  400. '<div><input type="file" id="file" name="file" /></div>' +
  401. '</li><li id="customattachment" class="hidden"></li><li>' +
  402. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  403. '</li></ul></li></ul></div></nav>';
  404. PrivateBin.TopNav.init();
  405. results.push(
  406. query('#customattachment').classList.contains('hidden')
  407. );
  408. PrivateBin.TopNav.showCustomAttachment();
  409. results.push(
  410. !query('#customattachment').classList.contains('hidden')
  411. );
  412. cleanup();
  413. const result = results.every(element => element);
  414. if (!result) {
  415. console.log(results);
  416. }
  417. assert.ok(result);
  418. }
  419. );
  420. });
  421. describe('collapseBar', function () {
  422. before(function () {
  423. cleanup();
  424. });
  425. it(
  426. 'collapses the navigation when displayed on a small screen',
  427. function () {
  428. let results = [];
  429. document.documentElement.innerHTML =
  430. '<nav><div class="navbar-header"><button type="button" ' +
  431. 'class="navbar-toggle collapsed" data-toggle="collapse" ' +
  432. 'data-target="#navbar" aria-expanded="false" aria-controls' +
  433. '="navbar">Toggle navigation</button><a class="reloadlink ' +
  434. 'navbar-brand" href=""><img alt="PrivateBin" ' +
  435. 'src="img/icon.svg" width="38" /></a></div><div ' +
  436. 'id="navbar"><ul><li><button id="newbutton" type=' +
  437. '"button" class="hidden">New</button></li></ul></div></nav>';
  438. PrivateBin.TopNav.init();
  439. results.push(
  440. query('.navbar-toggle').classList.contains('collapsed') &&
  441. query('#navbar').getAttribute('aria-expanded') !== 'true'
  442. );
  443. PrivateBin.TopNav.collapseBar();
  444. results.push(
  445. query('.navbar-toggle').classList.contains('collapsed') &&
  446. query('#navbar').getAttribute('aria-expanded') !== 'true'
  447. );
  448. /*
  449. with the upgrade for bootstrap-3.3.7.js to bootstrap-3.4.1.js
  450. the mobile interface detection changed to check if the
  451. ontouchstart event exists, which broke this section of the test
  452. const toggleBtn = document.querySelector('.navbar-toggle');
  453. toggleBtn.dispatchEvent(new MouseEvent('click'));
  454. results.push(
  455. !toggleBtn.classList.contains('collapsed') &&
  456. document.getElementById('navbar').getAttribute('aria-expanded') == 'true'
  457. );
  458. PrivateBin.TopNav.collapseBar();
  459. results.push(
  460. document.querySelector('.navbar-toggle').classList.contains('collapsed') &&
  461. document.getElementById('navbar').getAttribute('aria-expanded') == 'false'
  462. );
  463. */
  464. const result = results.every(element => element);
  465. if (!result) {
  466. console.log(results);
  467. }
  468. assert.ok(result);
  469. }
  470. );
  471. });
  472. describe('resetInput', function () {
  473. before(function () {
  474. cleanup();
  475. });
  476. it(
  477. 'reset inputs to defaults (options off)',
  478. function () {
  479. let results = [];
  480. document.documentElement.innerHTML =
  481. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  482. 'class="hidden"><label><input type="checkbox" ' +
  483. 'id="burnafterreading" name="burnafterreading" /> ' +
  484. 'Burn after reading</label></li><li id="opendiscussionoption" ' +
  485. 'class="hidden"><label><input type="checkbox" ' +
  486. 'id="opendiscussion" name="opendiscussion" /> ' +
  487. 'Open discussion</label></li></ul></div></nav>';
  488. PrivateBin.TopNav.init();
  489. results.push(
  490. !PrivateBin.TopNav.getBurnAfterReading()
  491. );
  492. results.push(
  493. !PrivateBin.TopNav.getOpenDiscussion()
  494. );
  495. query('#burnafterreading').checked = true;
  496. query('#opendiscussion').checked = true;
  497. results.push(
  498. PrivateBin.TopNav.getBurnAfterReading()
  499. );
  500. results.push(
  501. PrivateBin.TopNav.getOpenDiscussion()
  502. );
  503. PrivateBin.TopNav.resetInput();
  504. results.push(
  505. !PrivateBin.TopNav.getBurnAfterReading()
  506. );
  507. results.push(
  508. !PrivateBin.TopNav.getOpenDiscussion()
  509. );
  510. cleanup();
  511. const result = results.every(element => element);
  512. if (!result) {
  513. console.log(results);
  514. }
  515. assert.ok(result);
  516. }
  517. );
  518. it(
  519. 'reset inputs to defaults (burnafterreading on)',
  520. function () {
  521. let results = [];
  522. document.documentElement.innerHTML =
  523. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  524. 'class="hidden"><label><input type="checkbox" ' +
  525. 'id="burnafterreading" name="burnafterreading" checked="checked" /> ' +
  526. 'Burn after reading</label></li><li id="opendiscussionoption" ' +
  527. 'class="hidden"><label><input type="checkbox" ' +
  528. 'id="opendiscussion" name="opendiscussion" checked="checked" /> ' +
  529. 'Open discussion</label></li></ul></div></nav>';
  530. PrivateBin.TopNav.init();
  531. results.push(
  532. PrivateBin.TopNav.getBurnAfterReading()
  533. );
  534. results.push(
  535. !PrivateBin.TopNav.getOpenDiscussion()
  536. );
  537. query('#burnafterreading').checked = false;
  538. query('#burnafterreading').removeAttribute('checked');
  539. results.push(
  540. !PrivateBin.TopNav.getBurnAfterReading()
  541. );
  542. results.push(
  543. !PrivateBin.TopNav.getOpenDiscussion()
  544. );
  545. PrivateBin.TopNav.resetInput();
  546. results.push(
  547. PrivateBin.TopNav.getBurnAfterReading()
  548. );
  549. results.push(
  550. !PrivateBin.TopNav.getOpenDiscussion()
  551. );
  552. cleanup();
  553. const result = results.every(element => element);
  554. if (!result) {
  555. console.log(results);
  556. }
  557. assert.ok(result);
  558. }
  559. );
  560. it(
  561. 'reset inputs to defaults (opendiscussion on)',
  562. function () {
  563. let results = [];
  564. document.documentElement.innerHTML =
  565. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  566. 'class="hidden"><label><input type="checkbox" ' +
  567. 'id="burnafterreading" name="burnafterreading" /> ' +
  568. 'Burn after reading</label></li><li id="opendiscussionoption" ' +
  569. 'class="hidden"><label><input type="checkbox" ' +
  570. 'id="opendiscussion" name="opendiscussion" checked="checked" /> ' +
  571. 'Open discussion</label></li></ul></div></nav>';
  572. PrivateBin.TopNav.init();
  573. results.push(
  574. !PrivateBin.TopNav.getBurnAfterReading()
  575. );
  576. results.push(
  577. PrivateBin.TopNav.getOpenDiscussion()
  578. );
  579. query('#opendiscussion').checked = false;
  580. query('#opendiscussion').removeAttribute('checked');
  581. query('#burnafterreading').checked = true;
  582. query('#burnafterreading').setAttribute('checked', 'checked');
  583. results.push(
  584. PrivateBin.TopNav.getBurnAfterReading()
  585. );
  586. results.push(
  587. !PrivateBin.TopNav.getOpenDiscussion()
  588. );
  589. PrivateBin.TopNav.resetInput();
  590. results.push(
  591. !PrivateBin.TopNav.getBurnAfterReading()
  592. );
  593. results.push(
  594. PrivateBin.TopNav.getOpenDiscussion()
  595. );
  596. cleanup();
  597. const result = results.every(element => element);
  598. if (!result) {
  599. console.log(results);
  600. }
  601. assert.ok(result);
  602. }
  603. );
  604. it(
  605. 'tolerates a missing discussion option (discussion disabled)',
  606. function () {
  607. document.documentElement.innerHTML =
  608. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  609. 'class="buttondisabled"><label><input type="checkbox" ' +
  610. 'id="burnafterreading" name="burnafterreading" /> ' +
  611. 'Burn after reading</label></li></ul></div></nav>';
  612. PrivateBin.TopNav.init();
  613. assert.doesNotThrow(() => PrivateBin.TopNav.resetInput());
  614. assert.ok(!query('#burnafterreadingoption').classList.contains('buttondisabled'));
  615. cleanup();
  616. }
  617. );
  618. });
  619. describe('getExpiration', function () {
  620. before(function () {
  621. cleanup();
  622. });
  623. it(
  624. 'returns the currently selected expiration date',
  625. function () {
  626. document.documentElement.innerHTML =
  627. '<select id="pasteExpiration" name="pasteExpiration">' +
  628. '<option value="1day">1 day</option>' +
  629. '<option value="never">Never</option></select>';
  630. PrivateBin.TopNav.init();
  631. assert.strictEqual(PrivateBin.TopNav.getExpiration(), '1day');
  632. cleanup();
  633. }
  634. );
  635. });
  636. describe('getFileList', function () {
  637. before(function () {
  638. cleanup();
  639. });
  640. const File = window.File,
  641. FileList = window.FileList,
  642. path = require('path'), // eslint-disable-line global-require
  643. mime = require('mime-types'); // eslint-disable-line global-require
  644. // mocking file input as per https://github.com/jsdom/jsdom/issues/1272
  645. function createFile(file_path) {
  646. const lastModified = fs.statSync(file_path).mtimeMs;
  647. return new File(
  648. [new fs.readFileSync(file_path)],
  649. path.basename(file_path),
  650. {
  651. lastModified,
  652. type: mime.lookup(file_path) || ''
  653. }
  654. );
  655. }
  656. function addFileList(input, file_paths) {
  657. if (typeof file_paths === 'string') {
  658. file_paths = [file_paths];
  659. } else if (!Array.isArray(file_paths)) {
  660. throw new Error('file_paths needs to be a file path string or an Array of file path strings');
  661. }
  662. const file_list = file_paths.map(fp => createFile(fp));
  663. Object.setPrototypeOf(file_list, Object.create(FileList.prototype));
  664. Object.defineProperty(input, 'files', {
  665. value: file_list,
  666. writeable: false
  667. });
  668. return input;
  669. }
  670. it(
  671. 'returns the selected files',
  672. function () {
  673. let results = [];
  674. document.documentElement.innerHTML =
  675. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  676. 'dropdown"><a href="#" class="dropdown-toggle" data-' +
  677. 'toggle="dropdown" role="button" aria-haspopup="true" ' +
  678. 'aria-expanded="false">Attach a file <span class="caret">' +
  679. '</span></a><ul class="dropdown-menu"><li id="filewrap">' +
  680. '<div><input type="file" id="file" name="file" /></div>' +
  681. '</li><li id="customattachment" class="hidden"></li><li>' +
  682. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  683. '</li></ul></li></ul></div></nav>';
  684. PrivateBin.TopNav.init();
  685. results.push(
  686. PrivateBin.TopNav.getFileList() === null
  687. );
  688. addFileList(query('#file'), [
  689. '../img/logo.svg',
  690. '../img/busy.gif'
  691. ]);
  692. const files = PrivateBin.TopNav.getFileList();
  693. results.push(
  694. files[0].name === 'logo.svg' &&
  695. files[1].name === 'busy.gif'
  696. );
  697. cleanup();
  698. const result = results.every(element => element);
  699. if (!result) {
  700. console.log(results);
  701. }
  702. assert.ok(result);
  703. }
  704. );
  705. });
  706. describe('getBurnAfterReading', function () {
  707. before(function () {
  708. cleanup();
  709. });
  710. it(
  711. 'returns if the burn-after-reading checkbox was ticked',
  712. function () {
  713. let results = [];
  714. document.documentElement.innerHTML =
  715. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  716. 'class="hidden"><label><input type="checkbox" ' +
  717. 'id="burnafterreading" name="burnafterreading" /> ' +
  718. 'Burn after reading</label></li></ul></div></nav>';
  719. PrivateBin.TopNav.init();
  720. results.push(
  721. !PrivateBin.TopNav.getBurnAfterReading()
  722. );
  723. query('#burnafterreading').checked = true;
  724. results.push(
  725. PrivateBin.TopNav.getBurnAfterReading()
  726. );
  727. query('#burnafterreading').checked = false;
  728. query('#burnafterreading').removeAttribute('checked');
  729. results.push(
  730. !PrivateBin.TopNav.getBurnAfterReading()
  731. );
  732. cleanup();
  733. const result = results.every(element => element);
  734. if (!result) {
  735. console.log(results);
  736. }
  737. assert.ok(result);
  738. }
  739. );
  740. });
  741. describe('getOpenDiscussion', function () {
  742. before(function () {
  743. cleanup();
  744. });
  745. it(
  746. 'returns if the open-discussion checkbox was ticked',
  747. function () {
  748. let results = [];
  749. document.documentElement.innerHTML =
  750. '<nav><div id="navbar"><ul><li id="opendiscussionoption" ' +
  751. 'class="hidden"><label><input type="checkbox" ' +
  752. 'id="opendiscussion" name="opendiscussion" /> ' +
  753. 'Open discussion</label></li></ul></div></nav>';
  754. PrivateBin.TopNav.init();
  755. results.push(
  756. !PrivateBin.TopNav.getOpenDiscussion()
  757. );
  758. query('#opendiscussion').checked = true;
  759. results.push(
  760. PrivateBin.TopNav.getOpenDiscussion()
  761. );
  762. query('#opendiscussion').checked = false;
  763. query('#opendiscussion').removeAttribute('checked');
  764. results.push(
  765. !PrivateBin.TopNav.getOpenDiscussion()
  766. );
  767. cleanup();
  768. const result = results.every(element => element);
  769. if (!result) {
  770. console.log(results);
  771. }
  772. assert.ok(result);
  773. }
  774. );
  775. });
  776. describe('getPassword', function () {
  777. before(function () {
  778. globalThis.cleanup();
  779. });
  780. it('returns the contents of the password input', () => {
  781. fc.assert(fc.property(
  782. fc.string(),
  783. function (password) {
  784. password = password.replace(/\r+|\n+/g, '');
  785. let results = [];
  786. document.documentElement.innerHTML =
  787. '<nav><div id="navbar"><ul><li><div id="password" ' +
  788. 'class="navbar-form hidden"><input type="password" ' +
  789. 'id="passwordinput" placeholder="Password (recommended)" ' +
  790. 'class="form-control" size="23" /></div></li></ul></div></nav>';
  791. PrivateBin.TopNav.init();
  792. results.push(
  793. PrivateBin.TopNav.getPassword() === ''
  794. );
  795. query('#passwordinput').value = password;
  796. results.push(
  797. PrivateBin.TopNav.getPassword() === password
  798. );
  799. query('#passwordinput').value = '';
  800. results.push(
  801. PrivateBin.TopNav.getPassword() === ''
  802. );
  803. globalThis.cleanup();
  804. const result = results.every(element => element);
  805. if (!result) {
  806. console.log(results);
  807. }
  808. return result;
  809. }
  810. ));
  811. });
  812. });
  813. describe('getCustomAttachment', function () {
  814. before(function () {
  815. cleanup();
  816. });
  817. it(
  818. 'returns the custom attachment element',
  819. function () {
  820. let results = [];
  821. document.documentElement.innerHTML =
  822. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  823. 'dropdown"><a href="#" class="dropdown-toggle" data-' +
  824. 'toggle="dropdown" role="button" aria-haspopup="true" ' +
  825. 'aria-expanded="false">Attach a file <span class="caret">' +
  826. '</span></a><ul class="dropdown-menu"><li id="filewrap">' +
  827. '<div><input type="file" id="file" name="file" /></div>' +
  828. '</li><li id="customattachment" class="hidden"></li><li>' +
  829. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  830. '</li></ul></li></ul></div></nav>';
  831. PrivateBin.TopNav.init();
  832. results.push(
  833. !PrivateBin.TopNav.getCustomAttachment().classList.contains('test')
  834. );
  835. query('#customattachment').classList.add('test');
  836. results.push(
  837. PrivateBin.TopNav.getCustomAttachment().classList.contains('test')
  838. );
  839. cleanup();
  840. const result = results.every(element => element);
  841. if (!result) {
  842. console.log(results);
  843. }
  844. assert.ok(result);
  845. }
  846. );
  847. });
  848. describe('hideAllButtons', function () {
  849. before(function () {
  850. cleanup();
  851. });
  852. it(
  853. 'hides all buttons correctly',
  854. function () {
  855. // Insert any setup code needed for the hideAllButtons function
  856. // Example: Initialize the DOM elements required for testing
  857. document.body.innerHTML =
  858. '<nav class="navbar navbar-inverse navbar-static-top">' +
  859. '<div id="navbar" class="navbar-collapse collapse"><ul ' +
  860. 'class="nav navbar-nav"><li><button id="newbutton" ' +
  861. 'type="button" class="hidden btn btn-warning navbar-btn">' +
  862. '<span class="glyphicon glyphicon-file" aria-hidden="true">' +
  863. '</span> New</button><button id="clonebutton" type="button"' +
  864. ' class="hidden btn btn-warning navbar-btn">' +
  865. '<span class="glyphicon glyphicon-duplicate" ' +
  866. 'aria-hidden="true"></span> Clone</button><button ' +
  867. 'id="rawtextbutton" type="button" class="hidden btn ' +
  868. 'btn-warning navbar-btn"><span class="glyphicon ' +
  869. 'glyphicon-text-background" aria-hidden="true"></span> ' +
  870. 'Raw text</button><button id="qrcodelink" type="button" ' +
  871. 'data-toggle="modal" data-target="#qrcodemodal" ' +
  872. 'class="hidden btn btn-warning navbar-btn"><span ' +
  873. 'class="glyphicon glyphicon-qrcode" aria-hidden="true">' +
  874. '</span> QR code</button></li></ul></div></nav>';
  875. PrivateBin.TopNav.init();
  876. PrivateBin.TopNav.hideAllButtons();
  877. assert.ok(query('#newbutton').classList.contains('hidden'));
  878. assert.ok(query('#clonebutton').classList.contains('hidden'));
  879. assert.ok(query('#rawtextbutton').classList.contains('hidden'));
  880. assert.ok(query('#qrcodelink').classList.contains('hidden'));
  881. cleanup();
  882. }
  883. );
  884. });
  885. describe('showRetryButton & hideRetryButton', function () {
  886. afterEach(function () {
  887. delete global.bootstrap;
  888. cleanup();
  889. });
  890. it(
  891. 'shows and hides the Bootstrap 5 retry button with its list item and runs the callback once',
  892. function () {
  893. cleanup();
  894. global.bootstrap = {};
  895. document.documentElement.innerHTML =
  896. '<nav><div id="navbar"><ul class="navbar-nav">' +
  897. '<li class="nav-item hidden"><button id="retrybutton" ' +
  898. 'type="button" class="hidden">Retry</button></li>' +
  899. '<li><button id="newbutton" type="button">New</button>' +
  900. '</li></ul></div></nav>';
  901. PrivateBin.TopNav.init();
  902. let calls = 0;
  903. PrivateBin.TopNav.setRetryCallback(function () {
  904. calls++;
  905. });
  906. const retryItem = query('#retrybutton').parentElement;
  907. assert.ok(query('#retrybutton').classList.contains('hidden'));
  908. assert.ok(retryItem.classList.contains('hidden'));
  909. PrivateBin.TopNav.showRetryButton();
  910. assert.ok(!query('#retrybutton').classList.contains('hidden'));
  911. assert.ok(!retryItem.classList.contains('hidden'));
  912. query('#retrybutton').click();
  913. assert.strictEqual(calls, 1);
  914. PrivateBin.TopNav.hideRetryButton();
  915. assert.ok(query('#retrybutton').classList.contains('hidden'));
  916. assert.ok(retryItem.classList.contains('hidden'));
  917. PrivateBin.TopNav.showRetryButton();
  918. PrivateBin.TopNav.hideRetryButton();
  919. assert.ok(query('#retrybutton').classList.contains('hidden'));
  920. assert.ok(retryItem.classList.contains('hidden'));
  921. assert.ok(!query('#newbutton').classList.contains('hidden'));
  922. assert.strictEqual(calls, 1);
  923. }
  924. );
  925. it(
  926. 'toggles only the retry button on legacy Bootstrap 3 and page templates',
  927. function () {
  928. cleanup();
  929. delete global.bootstrap;
  930. document.documentElement.innerHTML =
  931. '<nav><div id="navbar"><ul class="nav navbar-nav"><li>' +
  932. '<button id="retrybutton" type="button" class="hidden">' +
  933. 'Retry</button><button id="newbutton" type="button">' +
  934. 'New</button></li></ul></div></nav>';
  935. PrivateBin.TopNav.init();
  936. let calls = 0;
  937. PrivateBin.TopNav.setRetryCallback(function () {
  938. calls++;
  939. });
  940. const retryItem = query('#retrybutton').parentElement;
  941. assert.ok(query('#retrybutton').classList.contains('hidden'));
  942. assert.ok(!retryItem.classList.contains('hidden'));
  943. PrivateBin.TopNav.showRetryButton();
  944. assert.ok(!query('#retrybutton').classList.contains('hidden'));
  945. assert.ok(!retryItem.classList.contains('hidden'));
  946. assert.ok(!query('#newbutton').classList.contains('hidden'));
  947. query('#retrybutton').click();
  948. assert.strictEqual(calls, 1);
  949. PrivateBin.TopNav.hideRetryButton();
  950. assert.ok(query('#retrybutton').classList.contains('hidden'));
  951. assert.ok(!retryItem.classList.contains('hidden'));
  952. assert.ok(!query('#newbutton').classList.contains('hidden'));
  953. PrivateBin.TopNav.showRetryButton();
  954. PrivateBin.TopNav.hideRetryButton();
  955. assert.ok(query('#retrybutton').classList.contains('hidden'));
  956. assert.strictEqual(calls, 1);
  957. }
  958. );
  959. });
  960. describe('rawText', function () {
  961. before(function () {
  962. cleanup();
  963. });
  964. // TODO triggers error messages in jsDOM since version 12, but passes
  965. it(
  966. 'displays raw text view correctly',
  967. function () {
  968. const clean = globalThis.cleanup('', {url: 'https://privatebin.net/?0123456789abcdef#1'});
  969. document.documentElement.innerHTML = `
  970. <li id="loadingindicator" class="hidden"></li>
  971. <button id="rawtextbutton"></button>`;
  972. const sample = 'example';
  973. PrivateBin.Alert.init(); // required because of locading indiator being used
  974. PrivateBin.TopNav.init();
  975. PrivateBin.PasteViewer.setText(sample);
  976. PrivateBin.Helper.reset();
  977. query('#rawtextbutton').click();
  978. assert.strictEqual(document.querySelector('pre').textContent, sample);
  979. clean();
  980. }
  981. );
  982. });
  983. });