TopNav.js 44 KB

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