TopNav.js 50 KB

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