TopNav.js 43 KB

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