TopNav.js 45 KB

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