TopNav.js 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  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="nav-link dropdown-toggle" ' +
  381. 'data-bs-toggle="dropdown" role="button" ' +
  382. 'aria-expanded="false">Attach a file</a>' +
  383. '<ul class="dropdown-menu px-2"><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('highlightFileupload', function () {
  406. afterEach(function () {
  407. cleanup();
  408. });
  409. it(
  410. 'opens the attachment dropdown and highlights the file selection',
  411. function () {
  412. cleanup();
  413. document.documentElement.innerHTML =
  414. '<nav><div id="navbar"><ul><li id="attach" class="nav-item ' +
  415. 'dropdown"><a href="#" class="nav-link dropdown-toggle" ' +
  416. 'data-bs-toggle="dropdown" role="button" ' +
  417. 'aria-expanded="false">Attach a file</a>' +
  418. '<ul class="dropdown-menu px-2"><li id="filewrap">' +
  419. '<div><input type="file" id="file" name="file" /></div>' +
  420. '</li></ul></li></ul></div></nav>';
  421. PrivateBin.TopNav.init();
  422. const errors = [];
  423. const consoleError = console.error;
  424. console.error = function (...args) {
  425. errors.push(args.join(' '));
  426. };
  427. try {
  428. PrivateBin.TopNav.highlightFileupload();
  429. assert.strictEqual(query('.dropdown-toggle').getAttribute('aria-expanded'), 'true');
  430. assert.ok(query('.dropdown-menu').classList.contains('show'), 'dropdown is open');
  431. assert.ok(query('#filewrap').classList.contains('highlight'), 'file selection is highlighted');
  432. // a second upload, while the dropdown is still open
  433. PrivateBin.TopNav.highlightFileupload();
  434. assert.ok(query('.dropdown-menu').classList.contains('show'), 'dropdown stays open');
  435. // a third upload, after the user closed the dropdown
  436. query('.dropdown-toggle').click();
  437. assert.strictEqual(query('.dropdown-toggle').getAttribute('aria-expanded'), 'false');
  438. PrivateBin.TopNav.highlightFileupload();
  439. assert.ok(query('.dropdown-menu').classList.contains('show'), 'dropdown is open again');
  440. } finally {
  441. console.error = consoleError;
  442. }
  443. assert.deepStrictEqual(errors, []);
  444. }
  445. );
  446. });
  447. describe('showCustomAttachment', function () {
  448. before(function () {
  449. cleanup();
  450. });
  451. it(
  452. 'display the custom file attachment',
  453. function () {
  454. let results = [];
  455. document.documentElement.innerHTML =
  456. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  457. 'dropdown"><a href="#" class="nav-link dropdown-toggle" ' +
  458. 'data-bs-toggle="dropdown" role="button" ' +
  459. 'aria-expanded="false">Attach a file</a>' +
  460. '<ul class="dropdown-menu px-2"><li id="filewrap">' +
  461. '<div><input type="file" id="file" name="file" /></div>' +
  462. '</li><li id="customattachment" class="hidden"></li><li>' +
  463. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  464. '</li></ul></li></ul></div></nav>';
  465. PrivateBin.TopNav.init();
  466. results.push(
  467. query('#customattachment').classList.contains('hidden')
  468. );
  469. PrivateBin.TopNav.showCustomAttachment();
  470. results.push(
  471. !query('#customattachment').classList.contains('hidden')
  472. );
  473. cleanup();
  474. const result = results.every(element => element);
  475. if (!result) {
  476. console.log(results);
  477. }
  478. assert.ok(result);
  479. }
  480. );
  481. });
  482. describe('collapseBar', function () {
  483. before(function () {
  484. cleanup();
  485. });
  486. /**
  487. * navigation bar markup of the bootstrap5 template, with the
  488. * collapsible navigation either expanded or collapsed
  489. *
  490. * @param {boolean} isExpanded
  491. * @returns {string}
  492. */
  493. function navbar(isExpanded) {
  494. return '<nav class="navbar navbar-expand-lg"><div class="container-fluid">' +
  495. '<button class="navbar-toggler' + (isExpanded ? '' : ' collapsed') +
  496. '" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" ' +
  497. 'aria-controls="navbar" aria-expanded="' + isExpanded + '" ' +
  498. 'aria-label="Toggle navigation"><span class="navbar-toggler-icon">' +
  499. '</span></button><div id="navbar" class="collapse navbar-collapse' +
  500. (isExpanded ? ' show' : '') + '"><ul class="navbar-nav"><li>' +
  501. '<button id="newbutton" type="button" class="hidden">New</button>' +
  502. '</li></ul></div></div></nav>';
  503. }
  504. it(
  505. 'collapses the expanded navigation when displayed on a small screen',
  506. function () {
  507. document.documentElement.innerHTML = navbar(true);
  508. PrivateBin.TopNav.init();
  509. let clicks = 0;
  510. query('.navbar-toggler').addEventListener('click', function () {
  511. ++clicks;
  512. });
  513. PrivateBin.TopNav.collapseBar();
  514. assert.strictEqual(clicks, 1, 'toggler is clicked once');
  515. assert.ok(!query('#navbar').classList.contains('show'), 'navigation is collapsing');
  516. }
  517. );
  518. it(
  519. 'leaves the collapsed navigation alone',
  520. function () {
  521. document.documentElement.innerHTML = navbar(false);
  522. PrivateBin.TopNav.init();
  523. let clicks = 0;
  524. query('.navbar-toggler').addEventListener('click', function () {
  525. ++clicks;
  526. });
  527. PrivateBin.TopNav.collapseBar();
  528. assert.strictEqual(clicks, 0, 'toggler is not clicked');
  529. assert.ok(!query('#navbar').classList.contains('collapsing'), 'navigation is not toggled');
  530. }
  531. );
  532. it(
  533. 'ignores pages without a navigation',
  534. function () {
  535. document.documentElement.innerHTML = '<body></body>';
  536. PrivateBin.TopNav.collapseBar();
  537. }
  538. );
  539. });
  540. describe('resetInput', function () {
  541. before(function () {
  542. cleanup();
  543. });
  544. it(
  545. 'reset inputs to defaults (options off)',
  546. function () {
  547. let results = [];
  548. document.documentElement.innerHTML =
  549. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  550. 'class="hidden"><label><input type="checkbox" ' +
  551. 'id="burnafterreading" name="burnafterreading" /> ' +
  552. 'Burn after reading</label></li><li id="opendiscussionoption" ' +
  553. 'class="hidden"><label><input type="checkbox" ' +
  554. 'id="opendiscussion" name="opendiscussion" /> ' +
  555. 'Open discussion</label></li></ul></div></nav>';
  556. PrivateBin.TopNav.init();
  557. results.push(
  558. !PrivateBin.TopNav.getBurnAfterReading()
  559. );
  560. results.push(
  561. !PrivateBin.TopNav.getOpenDiscussion()
  562. );
  563. query('#burnafterreading').checked = true;
  564. query('#opendiscussion').checked = true;
  565. results.push(
  566. PrivateBin.TopNav.getBurnAfterReading()
  567. );
  568. results.push(
  569. PrivateBin.TopNav.getOpenDiscussion()
  570. );
  571. PrivateBin.TopNav.resetInput();
  572. results.push(
  573. !PrivateBin.TopNav.getBurnAfterReading()
  574. );
  575. results.push(
  576. !PrivateBin.TopNav.getOpenDiscussion()
  577. );
  578. cleanup();
  579. const result = results.every(element => element);
  580. if (!result) {
  581. console.log(results);
  582. }
  583. assert.ok(result);
  584. }
  585. );
  586. it(
  587. 'reset inputs to defaults (burnafterreading on)',
  588. function () {
  589. let results = [];
  590. document.documentElement.innerHTML =
  591. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  592. 'class="hidden"><label><input type="checkbox" ' +
  593. 'id="burnafterreading" name="burnafterreading" checked="checked" /> ' +
  594. 'Burn after reading</label></li><li id="opendiscussionoption" ' +
  595. 'class="hidden"><label><input type="checkbox" ' +
  596. 'id="opendiscussion" name="opendiscussion" checked="checked" /> ' +
  597. 'Open discussion</label></li></ul></div></nav>';
  598. PrivateBin.TopNav.init();
  599. results.push(
  600. PrivateBin.TopNav.getBurnAfterReading()
  601. );
  602. results.push(
  603. !PrivateBin.TopNav.getOpenDiscussion()
  604. );
  605. query('#burnafterreading').checked = false;
  606. query('#burnafterreading').removeAttribute('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. 'reset inputs to defaults (opendiscussion on)',
  630. function () {
  631. let results = [];
  632. document.documentElement.innerHTML =
  633. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  634. 'class="hidden"><label><input type="checkbox" ' +
  635. 'id="burnafterreading" name="burnafterreading" /> ' +
  636. 'Burn after reading</label></li><li id="opendiscussionoption" ' +
  637. 'class="hidden"><label><input type="checkbox" ' +
  638. 'id="opendiscussion" name="opendiscussion" checked="checked" /> ' +
  639. 'Open discussion</label></li></ul></div></nav>';
  640. PrivateBin.TopNav.init();
  641. results.push(
  642. !PrivateBin.TopNav.getBurnAfterReading()
  643. );
  644. results.push(
  645. PrivateBin.TopNav.getOpenDiscussion()
  646. );
  647. query('#opendiscussion').checked = false;
  648. query('#opendiscussion').removeAttribute('checked');
  649. query('#burnafterreading').checked = true;
  650. query('#burnafterreading').setAttribute('checked', 'checked');
  651. results.push(
  652. PrivateBin.TopNav.getBurnAfterReading()
  653. );
  654. results.push(
  655. !PrivateBin.TopNav.getOpenDiscussion()
  656. );
  657. PrivateBin.TopNav.resetInput();
  658. results.push(
  659. !PrivateBin.TopNav.getBurnAfterReading()
  660. );
  661. results.push(
  662. PrivateBin.TopNav.getOpenDiscussion()
  663. );
  664. cleanup();
  665. const result = results.every(element => element);
  666. if (!result) {
  667. console.log(results);
  668. }
  669. assert.ok(result);
  670. }
  671. );
  672. it(
  673. 'tolerates a missing discussion option (discussion disabled)',
  674. function () {
  675. document.documentElement.innerHTML =
  676. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  677. 'class="buttondisabled"><label><input type="checkbox" ' +
  678. 'id="burnafterreading" name="burnafterreading" /> ' +
  679. 'Burn after reading</label></li></ul></div></nav>';
  680. PrivateBin.TopNav.init();
  681. assert.doesNotThrow(() => PrivateBin.TopNav.resetInput());
  682. assert.ok(!query('#burnafterreadingoption').classList.contains('buttondisabled'));
  683. cleanup();
  684. }
  685. );
  686. });
  687. describe('getExpiration', function () {
  688. before(function () {
  689. cleanup();
  690. });
  691. it(
  692. 'returns the currently selected expiration date',
  693. function () {
  694. document.documentElement.innerHTML =
  695. '<select id="pasteExpiration" name="pasteExpiration">' +
  696. '<option value="1day">1 day</option>' +
  697. '<option value="never">Never</option></select>';
  698. PrivateBin.TopNav.init();
  699. assert.strictEqual(PrivateBin.TopNav.getExpiration(), '1day');
  700. cleanup();
  701. }
  702. );
  703. it(
  704. 'returns the expiration date the user selected',
  705. function () {
  706. document.documentElement.innerHTML =
  707. '<select id="pasteExpiration" name="pasteExpiration">' +
  708. '<option value="1day">1 day</option>' +
  709. '<option value="never">Never</option></select>';
  710. PrivateBin.TopNav.init();
  711. const select = query('#pasteExpiration');
  712. select.value = 'never';
  713. select.dispatchEvent(new window.Event('change'));
  714. assert.strictEqual(PrivateBin.TopNav.getExpiration(), 'never');
  715. cleanup();
  716. }
  717. );
  718. });
  719. describe('setFormat', function () {
  720. before(function () {
  721. cleanup();
  722. });
  723. const formatter =
  724. '<li id="formatter" class="nav-item d-flex">' +
  725. '<select id="pasteFormatter" name="pasteFormatter" class="form-select">' +
  726. '<option value="plaintext" selected="selected">Plain Text</option>' +
  727. '<option value="syntaxhighlighting">Source Code</option>' +
  728. '<option value="markdown">Markdown</option></select></li>';
  729. it(
  730. 'selects the given format',
  731. function () {
  732. fc.assert(fc.property(
  733. fc.constantFrom('plaintext', 'syntaxhighlighting', 'markdown'),
  734. function (format) {
  735. document.documentElement.innerHTML = formatter;
  736. PrivateBin.TopNav.init();
  737. PrivateBin.TopNav.setFormat(format);
  738. assert.strictEqual(query('#pasteFormatter').value, format);
  739. }
  740. ));
  741. cleanup();
  742. }
  743. );
  744. it(
  745. 'tolerates a missing formatter (formatter selection disabled)',
  746. function () {
  747. document.documentElement.innerHTML = '<nav><div id="navbar"></div></nav>';
  748. PrivateBin.TopNav.init();
  749. assert.doesNotThrow(() => PrivateBin.TopNav.setFormat('markdown'));
  750. cleanup();
  751. }
  752. );
  753. it(
  754. 'passes the format the user selected to the viewer',
  755. function () {
  756. fc.assert(fc.property(
  757. fc.constantFrom('plaintext', 'syntaxhighlighting', 'markdown'),
  758. function (format) {
  759. cleanup();
  760. document.documentElement.innerHTML = formatter;
  761. PrivateBin.TopNav.init();
  762. const select = query('#pasteFormatter');
  763. select.value = format;
  764. select.dispatchEvent(new window.Event('change'));
  765. assert.strictEqual(PrivateBin.PasteViewer.getFormat(), format);
  766. }
  767. ));
  768. cleanup();
  769. }
  770. );
  771. });
  772. describe('getFileList', function () {
  773. before(function () {
  774. cleanup();
  775. });
  776. const File = window.File,
  777. FileList = window.FileList,
  778. path = require('path'), // eslint-disable-line global-require
  779. mime = require('mime-types'); // eslint-disable-line global-require
  780. // mocking file input as per https://github.com/jsdom/jsdom/issues/1272
  781. function createFile(file_path) {
  782. const lastModified = fs.statSync(file_path).mtimeMs;
  783. return new File(
  784. [new fs.readFileSync(file_path)],
  785. path.basename(file_path),
  786. {
  787. lastModified,
  788. type: mime.lookup(file_path) || ''
  789. }
  790. );
  791. }
  792. function addFileList(input, file_paths) {
  793. if (typeof file_paths === 'string') {
  794. file_paths = [file_paths];
  795. } else if (!Array.isArray(file_paths)) {
  796. throw new Error('file_paths needs to be a file path string or an Array of file path strings');
  797. }
  798. const file_list = file_paths.map(fp => createFile(fp));
  799. Object.setPrototypeOf(file_list, Object.create(FileList.prototype));
  800. Object.defineProperty(input, 'files', {
  801. value: file_list,
  802. writeable: false
  803. });
  804. return input;
  805. }
  806. it(
  807. 'returns the selected files',
  808. function () {
  809. let results = [];
  810. document.documentElement.innerHTML =
  811. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  812. 'dropdown"><a href="#" class="nav-link dropdown-toggle" ' +
  813. 'data-bs-toggle="dropdown" role="button" ' +
  814. 'aria-expanded="false">Attach a file</a>' +
  815. '<ul class="dropdown-menu px-2"><li id="filewrap">' +
  816. '<div><input type="file" id="file" name="file" /></div>' +
  817. '</li><li id="customattachment" class="hidden"></li><li>' +
  818. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  819. '</li></ul></li></ul></div></nav>';
  820. PrivateBin.TopNav.init();
  821. results.push(
  822. PrivateBin.TopNav.getFileList() === null
  823. );
  824. addFileList(query('#file'), [
  825. '../img/logo.svg',
  826. '../img/busy.gif'
  827. ]);
  828. const files = PrivateBin.TopNav.getFileList();
  829. results.push(
  830. files[0].name === 'logo.svg' &&
  831. files[1].name === 'busy.gif'
  832. );
  833. cleanup();
  834. const result = results.every(element => element);
  835. if (!result) {
  836. console.log(results);
  837. }
  838. assert.ok(result);
  839. }
  840. );
  841. });
  842. describe('getBurnAfterReading', function () {
  843. before(function () {
  844. cleanup();
  845. });
  846. it(
  847. 'returns if the burn-after-reading checkbox was ticked',
  848. function () {
  849. let results = [];
  850. document.documentElement.innerHTML =
  851. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  852. 'class="hidden"><label><input type="checkbox" ' +
  853. 'id="burnafterreading" name="burnafterreading" /> ' +
  854. 'Burn after reading</label></li></ul></div></nav>';
  855. PrivateBin.TopNav.init();
  856. results.push(
  857. !PrivateBin.TopNav.getBurnAfterReading()
  858. );
  859. query('#burnafterreading').checked = true;
  860. results.push(
  861. PrivateBin.TopNav.getBurnAfterReading()
  862. );
  863. query('#burnafterreading').checked = false;
  864. query('#burnafterreading').removeAttribute('checked');
  865. results.push(
  866. !PrivateBin.TopNav.getBurnAfterReading()
  867. );
  868. cleanup();
  869. const result = results.every(element => element);
  870. if (!result) {
  871. console.log(results);
  872. }
  873. assert.ok(result);
  874. }
  875. );
  876. });
  877. describe('getOpenDiscussion', function () {
  878. before(function () {
  879. cleanup();
  880. });
  881. it(
  882. 'returns if the open-discussion checkbox was ticked',
  883. function () {
  884. let results = [];
  885. document.documentElement.innerHTML =
  886. '<nav><div id="navbar"><ul><li id="opendiscussionoption" ' +
  887. 'class="hidden"><label><input type="checkbox" ' +
  888. 'id="opendiscussion" name="opendiscussion" /> ' +
  889. 'Open discussion</label></li></ul></div></nav>';
  890. PrivateBin.TopNav.init();
  891. results.push(
  892. !PrivateBin.TopNav.getOpenDiscussion()
  893. );
  894. query('#opendiscussion').checked = true;
  895. results.push(
  896. PrivateBin.TopNav.getOpenDiscussion()
  897. );
  898. query('#opendiscussion').checked = false;
  899. query('#opendiscussion').removeAttribute('checked');
  900. results.push(
  901. !PrivateBin.TopNav.getOpenDiscussion()
  902. );
  903. cleanup();
  904. const result = results.every(element => element);
  905. if (!result) {
  906. console.log(results);
  907. }
  908. assert.ok(result);
  909. }
  910. );
  911. });
  912. describe('getPassword', function () {
  913. before(function () {
  914. globalThis.cleanup();
  915. });
  916. it('returns the contents of the password input', () => {
  917. fc.assert(fc.property(
  918. fc.string(),
  919. function (password) {
  920. password = password.replace(/\r+|\n+/g, '');
  921. let results = [];
  922. document.documentElement.innerHTML =
  923. '<nav><div id="navbar"><ul><li><div id="password" ' +
  924. 'class="navbar-form hidden"><input type="password" ' +
  925. 'id="passwordinput" placeholder="Password (recommended)" ' +
  926. 'class="form-control" size="23" /></div></li></ul></div></nav>';
  927. PrivateBin.TopNav.init();
  928. results.push(
  929. PrivateBin.TopNav.getPassword() === ''
  930. );
  931. query('#passwordinput').value = password;
  932. results.push(
  933. PrivateBin.TopNav.getPassword() === password
  934. );
  935. query('#passwordinput').value = '';
  936. results.push(
  937. PrivateBin.TopNav.getPassword() === ''
  938. );
  939. globalThis.cleanup();
  940. const result = results.every(element => element);
  941. if (!result) {
  942. console.log(results);
  943. }
  944. return result;
  945. }
  946. ));
  947. });
  948. });
  949. describe('getCustomAttachment', function () {
  950. before(function () {
  951. cleanup();
  952. });
  953. it(
  954. 'returns the custom attachment element',
  955. function () {
  956. let results = [];
  957. document.documentElement.innerHTML =
  958. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  959. 'dropdown"><a href="#" class="nav-link dropdown-toggle" ' +
  960. 'data-bs-toggle="dropdown" role="button" ' +
  961. 'aria-expanded="false">Attach a file</a>' +
  962. '<ul class="dropdown-menu px-2"><li id="filewrap">' +
  963. '<div><input type="file" id="file" name="file" /></div>' +
  964. '</li><li id="customattachment" class="hidden"></li><li>' +
  965. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  966. '</li></ul></li></ul></div></nav>';
  967. PrivateBin.TopNav.init();
  968. results.push(
  969. !PrivateBin.TopNav.getCustomAttachment().classList.contains('test')
  970. );
  971. query('#customattachment').classList.add('test');
  972. results.push(
  973. PrivateBin.TopNav.getCustomAttachment().classList.contains('test')
  974. );
  975. cleanup();
  976. const result = results.every(element => element);
  977. if (!result) {
  978. console.log(results);
  979. }
  980. assert.ok(result);
  981. }
  982. );
  983. });
  984. describe('hideAllButtons', function () {
  985. before(function () {
  986. cleanup();
  987. });
  988. it(
  989. 'hides all buttons correctly',
  990. function () {
  991. // Insert any setup code needed for the hideAllButtons function
  992. // Example: Initialize the DOM elements required for testing
  993. document.body.innerHTML =
  994. '<nav class="navbar navbar-inverse navbar-static-top">' +
  995. '<div id="navbar" class="navbar-collapse collapse"><ul ' +
  996. 'class="nav navbar-nav"><li><button id="newbutton" ' +
  997. 'type="button" class="hidden btn btn-warning navbar-btn">' +
  998. '<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#file-earmark"></use></svg>' +
  999. ' New</button><button id="clonebutton" type="button"' +
  1000. ' class="hidden btn btn-warning navbar-btn">' +
  1001. '<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#copy"></use></svg>' +
  1002. ' Clone</button><button ' +
  1003. 'id="rawtextbutton" type="button" class="hidden btn ' +
  1004. 'btn-warning navbar-btn"><svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#filetype-txt"></use></svg> ' +
  1005. 'Raw text</button><button id="qrcodelink" type="button" ' +
  1006. 'data-toggle="modal" data-target="#qrcodemodal" ' +
  1007. 'class="hidden btn btn-warning navbar-btn">' +
  1008. '<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#qr-code"></use></svg>' +
  1009. ' QR code</button></li></ul></div></nav>';
  1010. PrivateBin.TopNav.init();
  1011. PrivateBin.TopNav.hideAllButtons();
  1012. assert.ok(query('#newbutton').classList.contains('hidden'));
  1013. assert.ok(query('#clonebutton').classList.contains('hidden'));
  1014. assert.ok(query('#rawtextbutton').classList.contains('hidden'));
  1015. assert.ok(query('#qrcodelink').classList.contains('hidden'));
  1016. cleanup();
  1017. }
  1018. );
  1019. });
  1020. describe('showRetryButton & hideRetryButton', function () {
  1021. afterEach(function () {
  1022. cleanup();
  1023. });
  1024. it(
  1025. 'shows and hides the retry button with its list item and runs the callback once',
  1026. function () {
  1027. cleanup();
  1028. document.documentElement.innerHTML =
  1029. '<nav><div id="navbar"><ul class="navbar-nav">' +
  1030. '<li class="nav-item hidden"><button id="retrybutton" ' +
  1031. 'type="button" class="hidden">Retry</button></li>' +
  1032. '<li><button id="newbutton" type="button">New</button>' +
  1033. '</li></ul></div></nav>';
  1034. PrivateBin.TopNav.init();
  1035. let calls = 0;
  1036. PrivateBin.TopNav.setRetryCallback(function () {
  1037. calls++;
  1038. });
  1039. const retryItem = query('#retrybutton').parentElement;
  1040. assert.ok(query('#retrybutton').classList.contains('hidden'));
  1041. assert.ok(retryItem.classList.contains('hidden'));
  1042. PrivateBin.TopNav.showRetryButton();
  1043. assert.ok(!query('#retrybutton').classList.contains('hidden'));
  1044. assert.ok(!retryItem.classList.contains('hidden'));
  1045. query('#retrybutton').click();
  1046. assert.strictEqual(calls, 1);
  1047. PrivateBin.TopNav.hideRetryButton();
  1048. assert.ok(query('#retrybutton').classList.contains('hidden'));
  1049. assert.ok(retryItem.classList.contains('hidden'));
  1050. PrivateBin.TopNav.showRetryButton();
  1051. PrivateBin.TopNav.hideRetryButton();
  1052. assert.ok(query('#retrybutton').classList.contains('hidden'));
  1053. assert.ok(retryItem.classList.contains('hidden'));
  1054. assert.ok(!query('#newbutton').classList.contains('hidden'));
  1055. assert.strictEqual(calls, 1);
  1056. }
  1057. );
  1058. });
  1059. describe('rawText', function () {
  1060. before(function () {
  1061. cleanup();
  1062. });
  1063. // TODO triggers error messages in jsDOM since version 12, but passes
  1064. it(
  1065. 'displays raw text view correctly',
  1066. function () {
  1067. const clean = globalThis.cleanup('', {url: 'https://privatebin.net/?0123456789abcdef#1'});
  1068. document.documentElement.innerHTML = `
  1069. <li id="loadingindicator" class="hidden"></li>
  1070. <button id="rawtextbutton"></button>`;
  1071. const sample = 'example';
  1072. PrivateBin.Alert.init(); // required because of locading indiator being used
  1073. PrivateBin.TopNav.init();
  1074. PrivateBin.PasteViewer.setText(sample);
  1075. PrivateBin.Helper.reset();
  1076. query('#rawtextbutton').click();
  1077. assert.strictEqual(document.querySelector('pre').textContent, sample);
  1078. clean();
  1079. }
  1080. );
  1081. });
  1082. });