1
0

TopNav.js 46 KB

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