1
0

TopNav.js 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354
  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. /**
  11. * builds the navigation with the e-mail button and its confirmation dialog, as
  12. * in the bootstrap5 template
  13. *
  14. * @param {boolean} withShortUrl - add the link of a shortened document URL
  15. */
  16. function buildEmailDom(withShortUrl) {
  17. document.documentElement.innerHTML =
  18. '<nav><div id="navbar"><ul><li>' +
  19. '<button id="clonebutton" type="button" class="btn btn-secondary">' +
  20. '<svg width="16" height="16" fill="currentColor" aria-hidden="true">' +
  21. '<use href="img/bootstrap-icons.svg#copy"></use></svg> Clone</button>' +
  22. '</li><li>' +
  23. '<button id="emaillink" type="button" class="hidden btn btn-secondary">Email</button>' +
  24. '</li></ul></div></nav>' +
  25. '<input id="burnafterreadingoption" type="checkbox">' +
  26. (withShortUrl ?
  27. '<div id="pastelink">Your document is ' +
  28. '<a id="pasteurl" href="https://short.example/xYz">https://short.example/xYz</a> ' +
  29. '<span id="copyhint">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span></div>' :
  30. '') +
  31. '<div id="emailconfirmmodal" tabindex="-1" class="modal" role="dialog" aria-hidden="true">' +
  32. '<div class="modal-dialog" role="document"><div class="modal-content"><div class="modal-body row">' +
  33. '<button id="emailconfirm-timezone-current" type="button">Use Current Timezone</button>' +
  34. '<button id="emailconfirm-timezone-utc" type="button">Convert To UTC</button>' +
  35. '</div></div></div></div>';
  36. }
  37. /**
  38. * replaces window.open, to record the e-mails sent via mailto: URLs
  39. *
  40. * @returns {{getUrl: function, getCount: function, reset: function, restore: function}}
  41. */
  42. function mockWindowOpen() {
  43. const originalOpen = window.open;
  44. let openedUrl = null,
  45. openedCount = 0;
  46. window.open = function (url) {
  47. openedUrl = url;
  48. ++openedCount;
  49. return {};
  50. };
  51. return {
  52. getUrl: () => openedUrl,
  53. getCount: () => openedCount,
  54. reset: () => {
  55. openedUrl = null;
  56. openedCount = 0;
  57. },
  58. restore: () => {
  59. window.open = originalOpen;
  60. }
  61. };
  62. }
  63. /**
  64. * extracts and decodes a parameter of a mailto: URL
  65. *
  66. * @param {string} mailtoUrl
  67. * @param {string} name - i.e. subject or body
  68. * @returns {string}
  69. */
  70. function mailtoParameter(mailtoUrl, name) {
  71. assert.match(mailtoUrl, /^mailto:\?/, 'expected a mailto: URL');
  72. const match = mailtoUrl.match(new RegExp('[?&]' + name + '=([^&]*)'));
  73. assert.ok(match, 'expected the mailto: URL to have a ' + name + '= parameter');
  74. return decodeURIComponent(match[1]);
  75. }
  76. describe('TopNav', function () {
  77. describe('showEmailButton & hideEmailButton', function () {
  78. let windowOpen;
  79. beforeEach(function () {
  80. cleanup();
  81. windowOpen = mockWindowOpen();
  82. });
  83. afterEach(function () {
  84. windowOpen.restore();
  85. cleanup();
  86. });
  87. it('does nothing when the e-mail button is omitted from the page', function () {
  88. document.documentElement.innerHTML =
  89. '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
  90. 'type="button">New</button></li></ul></div></nav>';
  91. PrivateBin.TopNav.init();
  92. // cleanup() reloads PrivateBin, dropping this replacement
  93. let errorShown = false;
  94. PrivateBin.Alert.showError = function () {
  95. errorShown = true;
  96. };
  97. assert.doesNotThrow(() => PrivateBin.TopNav.showEmailButton());
  98. assert.strictEqual(errorShown, false);
  99. });
  100. it('sends a single e-mail after the confirmation dialog was dismissed', function () {
  101. buildEmailDom(false);
  102. PrivateBin.TopNav.init();
  103. PrivateBin.TopNav.showEmailButton(3600);
  104. // open the dialog and dismiss it without choosing a timezone
  105. query('#emaillink').click();
  106. assert.strictEqual(windowOpen.getCount(), 0, 'no e-mail before choosing a timezone');
  107. query('#emaillink').click();
  108. query('#emailconfirm-timezone-current').click();
  109. assert.strictEqual(windowOpen.getCount(), 1, 'exactly one e-mail with the current timezone');
  110. query('#emaillink').click();
  111. query('#emailconfirm-timezone-utc').click();
  112. assert.strictEqual(windowOpen.getCount(), 2, 'exactly one more e-mail with UTC');
  113. });
  114. it('sends a single e-mail per click after the button was shown again', function () {
  115. buildEmailDom(false);
  116. PrivateBin.TopNav.init();
  117. // i.e. sending a second document without reloading the page
  118. PrivateBin.TopNav.showEmailButton(0);
  119. PrivateBin.TopNav.showEmailButton(0);
  120. query('#emaillink').click();
  121. assert.strictEqual(windowOpen.getCount(), 1, 'exactly one e-mail per click');
  122. });
  123. it('uses the expiration of the document the button was last shown for', function () {
  124. buildEmailDom(false);
  125. PrivateBin.TopNav.init();
  126. // first document expires, the second never does
  127. PrivateBin.TopNav.showEmailButton(3600);
  128. PrivateBin.TopNav.showEmailButton(0);
  129. query('#emaillink').click();
  130. assert.strictEqual(windowOpen.getCount(), 1, 'sent directly, without confirmation dialog');
  131. assert.doesNotMatch(mailtoParameter(windowOpen.getUrl(), 'body'), /expire/i, 'body has no expiration');
  132. query('#emailconfirm-timezone-current').click();
  133. assert.strictEqual(windowOpen.getCount(), 1, 'no e-mail from a stale confirmation dialog');
  134. });
  135. it('sends no e-mail from a confirmation dialog opened for an earlier document', function () {
  136. buildEmailDom(false);
  137. PrivateBin.TopNav.init();
  138. PrivateBin.TopNav.showEmailButton(3600);
  139. // open the dialog, but start a new document instead of choosing
  140. query('#emaillink').click();
  141. PrivateBin.TopNav.hideEmailButton();
  142. PrivateBin.TopNav.showEmailButton(0);
  143. query('#emailconfirm-timezone-current').click();
  144. query('#emailconfirm-timezone-utc').click();
  145. assert.strictEqual(windowOpen.getCount(), 0, 'no e-mail from a stale confirmation dialog');
  146. });
  147. it('sends no e-mail after the button was hidden', function () {
  148. buildEmailDom(false);
  149. PrivateBin.TopNav.init();
  150. PrivateBin.TopNav.showEmailButton(0);
  151. PrivateBin.TopNav.hideEmailButton();
  152. // i.e. a keyboard shortcut or assistive technology activating it
  153. query('#emaillink').click();
  154. assert.strictEqual(windowOpen.getCount(), 0, 'no e-mail from a hidden button');
  155. });
  156. it('sends exactly one e-mail, regardless of how often the button was shown or the dialog dismissed', function () {
  157. fc.assert(fc.property(
  158. fc.integer({min: 1, max: 5}),
  159. fc.integer({min: 0, max: 5}),
  160. fc.constantFrom('current', 'utc'),
  161. function (shown, dismissed, timezone) {
  162. // replacing the markup is enough, init binds to the new elements
  163. buildEmailDom(false);
  164. PrivateBin.TopNav.init();
  165. for (let i = 0; i < shown; ++i) {
  166. PrivateBin.TopNav.showEmailButton(3600);
  167. }
  168. windowOpen.reset();
  169. for (let i = 0; i < dismissed; ++i) {
  170. query('#emaillink').click();
  171. }
  172. query('#emaillink').click();
  173. query('#emailconfirm-timezone-' + timezone).click();
  174. assert.strictEqual(windowOpen.getCount(), 1);
  175. }
  176. ));
  177. });
  178. });
  179. describe('sendEmail', function () {
  180. let windowOpen;
  181. beforeEach(function () {
  182. cleanup();
  183. windowOpen = mockWindowOpen();
  184. });
  185. afterEach(function () {
  186. windowOpen.restore();
  187. cleanup();
  188. });
  189. it('puts the short URL into the mail body, if there is one', function () {
  190. buildEmailDom(true);
  191. PrivateBin.TopNav.init();
  192. PrivateBin.TopNav.showEmailButton(0);
  193. assert.ok(!query('#emaillink').classList.contains('hidden'), 'e-mail button is shown');
  194. query('#emaillink').click();
  195. assert.ok(windowOpen.getUrl(), 'an e-mail was sent');
  196. const body = mailtoParameter(windowOpen.getUrl(), 'body');
  197. assert.match(body, /https:\/\/short\.example\/xYz/, 'body contains the short URL');
  198. assert.doesNotMatch(body, /undefined/, 'body never contains "undefined"');
  199. });
  200. it('puts the page URL into the mail body, if there is no short URL', function () {
  201. buildEmailDom(false);
  202. PrivateBin.TopNav.init();
  203. PrivateBin.TopNav.showEmailButton(0);
  204. assert.ok(!query('#emaillink').classList.contains('hidden'), 'e-mail button is shown');
  205. query('#emaillink').click();
  206. assert.ok(windowOpen.getUrl(), 'an e-mail was sent');
  207. const body = mailtoParameter(windowOpen.getUrl(), 'body');
  208. assert.match(body, new RegExp(window.location.href), 'body contains the page URL');
  209. assert.doesNotMatch(body, /undefined/, 'body never contains "undefined"');
  210. });
  211. it('uses the burn-after-reading state of the viewed document, not the instance default', function () {
  212. buildEmailDom(false);
  213. const burnAfterReading = document.createElement('input');
  214. burnAfterReading.id = 'burnafterreading';
  215. burnAfterReading.type = 'checkbox';
  216. burnAfterReading.checked = true;
  217. document.body.appendChild(burnAfterReading);
  218. PrivateBin.TopNav.init();
  219. PrivateBin.TopNav.showEmailButton(0, false);
  220. query('#emaillink').click();
  221. assert.doesNotMatch(
  222. mailtoParameter(windowOpen.getUrl(), 'body'),
  223. /only be accessed once/,
  224. 'body uses the metadata of the viewed document'
  225. );
  226. });
  227. it('names the instance in the mail subject', function () {
  228. buildEmailDom(false);
  229. // buildEmailDom() replaces the whole document, including its title
  230. document.title = 'My PrivateBin Instance';
  231. PrivateBin.TopNav.init();
  232. PrivateBin.TopNav.showEmailButton(0);
  233. query('#emaillink').click();
  234. assert.ok(windowOpen.getUrl(), 'an e-mail was sent');
  235. assert.match(mailtoParameter(windowOpen.getUrl(), 'subject'), /My PrivateBin Instance/);
  236. });
  237. it('names the instance in the mail subject after the expiration was confirmed', function () {
  238. buildEmailDom(true);
  239. document.title = 'My PrivateBin Instance';
  240. PrivateBin.TopNav.init();
  241. // a remaining time asks to confirm the timezone of the expiration
  242. PrivateBin.TopNav.showEmailButton(3600);
  243. query('#emaillink').click();
  244. query('#emailconfirm-timezone-current').click();
  245. assert.ok(windowOpen.getUrl(), 'an e-mail was sent');
  246. assert.match(mailtoParameter(windowOpen.getUrl(), 'subject'), /My PrivateBin Instance/);
  247. });
  248. it('percent-encodes the mail subject, i.e. spaces in the instance name', function () {
  249. buildEmailDom(false);
  250. document.title = 'My Cool Instance';
  251. PrivateBin.TopNav.init();
  252. PrivateBin.TopNav.showEmailButton(0);
  253. query('#emaillink').click();
  254. const rawSubjectParam = windowOpen.getUrl().split('&body=')[0];
  255. assert.strictEqual(
  256. rawSubjectParam,
  257. `mailto:?subject=${encodeURIComponent('Encrypted note on My Cool Instance')}`,
  258. 'raw subject parameter is exactly the encoded translated string'
  259. );
  260. assert.doesNotMatch(rawSubjectParam, / /, 'raw mailto: URL contains no literal space');
  261. assert.strictEqual(
  262. mailtoParameter(windowOpen.getUrl(), 'subject'),
  263. 'Encrypted note on My Cool Instance',
  264. 'decoded subject round-trips exactly'
  265. );
  266. });
  267. });
  268. describe('hideBurnAfterReadingButtons', function () {
  269. it(
  270. 'hides buttons when the QR code button is disabled',
  271. function () {
  272. document.documentElement.innerHTML =
  273. '<nav><button id="clonebutton"></button></nav>';
  274. PrivateBin.TopNav.init();
  275. assert.doesNotThrow(function () {
  276. PrivateBin.TopNav.hideBurnAfterReadingButtons();
  277. });
  278. assert.ok(query('#clonebutton').classList.contains('hidden'));
  279. cleanup();
  280. }
  281. );
  282. });
  283. describe('showViewButtons & hideViewButtons', function () {
  284. before(function () {
  285. cleanup();
  286. });
  287. it(
  288. 'shows and hides navigation elements when the QR code button is disabled',
  289. function () {
  290. document.documentElement.innerHTML =
  291. '<nav><button id="newbutton" class="hidden"></button>' +
  292. '<button id="clonebutton" class="hidden"></button>' +
  293. '<button id="rawtextbutton" class="hidden"></button>' +
  294. '<button id="downloadtextbutton" class="hidden"></button></nav>';
  295. PrivateBin.TopNav.init();
  296. assert.doesNotThrow(function () {
  297. PrivateBin.TopNav.showViewButtons();
  298. });
  299. assert.ok(!query('#newbutton').classList.contains('hidden'));
  300. assert.ok(!query('#clonebutton').classList.contains('hidden'));
  301. assert.ok(!query('#rawtextbutton').classList.contains('hidden'));
  302. assert.ok(!query('#downloadtextbutton').classList.contains('hidden'));
  303. assert.doesNotThrow(function () {
  304. PrivateBin.TopNav.hideViewButtons();
  305. });
  306. assert.ok(query('#newbutton').classList.contains('hidden'));
  307. assert.ok(query('#clonebutton').classList.contains('hidden'));
  308. assert.ok(query('#rawtextbutton').classList.contains('hidden'));
  309. assert.ok(query('#downloadtextbutton').classList.contains('hidden'));
  310. cleanup();
  311. }
  312. );
  313. it(
  314. 'displays & hides navigation elements for viewing an existing document',
  315. function () {
  316. let results = [];
  317. document.documentElement.innerHTML =
  318. `<nav class="navbar navbar-inverse navbar-static-top">
  319. <div id="navbar" class="navbar-collapse collapse">
  320. <ul class="nav navbar-nav"><li><button id="newbutton" type="button" class="hidden btn btn-warning navbar-btn">
  321. <span class="glyphicon glyphicon-file" aria-hidden="true">
  322. </span> New</button><button id="clonebutton" type="button" class="hidden btn btn-warning navbar-btn">
  323. <span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> Clone</button>
  324. <button id="rawtextbutton" type="button" class="hidden btn btn-warning navbar-btn">
  325. <span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> Raw text</button>
  326. <button id="downloadtextbutton" type="button" class="hidden btn btn-default navbar-btn"></button>
  327. <button id="qrcodelink" type="button" data-toggle="modal" data-target="#qrcodemodal" class="hidden btn btn-warning navbar-btn"/>
  328. <span class="glyphicon glyphicon-qrcode" aria-hidden="true"></span> QR code</button></li></ul></div>
  329. </nav>`;
  330. PrivateBin.TopNav.init();
  331. results.push(
  332. query('#newbutton').classList.contains('hidden') &&
  333. query('#clonebutton').classList.contains('hidden') &&
  334. query('#rawtextbutton').classList.contains('hidden') &&
  335. query('#qrcodelink').classList.contains('hidden')
  336. );
  337. PrivateBin.TopNav.showViewButtons();
  338. results.push(
  339. !query('#newbutton').classList.contains('hidden') &&
  340. !query('#clonebutton').classList.contains('hidden') &&
  341. !query('#rawtextbutton').classList.contains('hidden') &&
  342. !query('#qrcodelink').classList.contains('hidden')
  343. );
  344. PrivateBin.TopNav.hideViewButtons();
  345. results.push(
  346. query('#newbutton').classList.contains('hidden') &&
  347. query('#clonebutton').classList.contains('hidden') &&
  348. query('#rawtextbutton').classList.contains('hidden') &&
  349. query('#qrcodelink').classList.contains('hidden')
  350. );
  351. cleanup();
  352. const result = results.every(element => element);
  353. if (!result) {
  354. console.log(results);
  355. }
  356. assert.ok(result);
  357. }
  358. );
  359. });
  360. describe('showCreateButtons & hideCreateButtons', function () {
  361. before(function () {
  362. cleanup();
  363. });
  364. it(
  365. 'displays & hides navigation elements for creating a document',
  366. function () { // eslint-disable-line complexity
  367. let results = [];
  368. document.documentElement.innerHTML =
  369. '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
  370. 'type="button" class="hidden">New</button></li><li><a ' +
  371. 'id="expiration" href="#" class="hidden">Expiration</a>' +
  372. '</li><li><div id="burnafterreadingoption" class="hidden">' +
  373. 'Burn after reading</div></li><li><div id="opendiscussion' +
  374. 'option" class="hidden">Open discussion</div></li><li>' +
  375. '<div id="password" class="hidden">Password</div></li>' +
  376. '<li id="attach" class="hidden">Attach a file</li><li>' +
  377. '<a id="formatter" href="#" class="hidden">Format</a>' +
  378. '</li><li><button id="sendbutton" type="button" ' +
  379. 'class="hidden">Create</button></li></ul></div></nav>';
  380. PrivateBin.TopNav.init();
  381. results.push(
  382. query('#sendbutton').classList.contains('hidden') &&
  383. query('#expiration').classList.contains('hidden') &&
  384. query('#formatter').classList.contains('hidden') &&
  385. query('#burnafterreadingoption').classList.contains('hidden') &&
  386. query('#opendiscussionoption').classList.contains('hidden') &&
  387. query('#newbutton').classList.contains('hidden') &&
  388. query('#password').classList.contains('hidden') &&
  389. query('#attach').classList.contains('hidden')
  390. );
  391. PrivateBin.TopNav.showCreateButtons();
  392. results.push(
  393. !query('#sendbutton').classList.contains('hidden') &&
  394. !query('#expiration').classList.contains('hidden') &&
  395. !query('#formatter').classList.contains('hidden') &&
  396. !query('#burnafterreadingoption').classList.contains('hidden') &&
  397. !query('#opendiscussionoption').classList.contains('hidden') &&
  398. !query('#newbutton').classList.contains('hidden') &&
  399. !query('#password').classList.contains('hidden') &&
  400. !query('#attach').classList.contains('hidden')
  401. );
  402. PrivateBin.TopNav.hideCreateButtons();
  403. results.push(
  404. query('#sendbutton').classList.contains('hidden') &&
  405. query('#expiration').classList.contains('hidden') &&
  406. query('#formatter').classList.contains('hidden') &&
  407. query('#burnafterreadingoption').classList.contains('hidden') &&
  408. query('#opendiscussionoption').classList.contains('hidden') &&
  409. query('#newbutton').classList.contains('hidden') &&
  410. query('#password').classList.contains('hidden') &&
  411. query('#attach').classList.contains('hidden')
  412. );
  413. cleanup();
  414. const result = results.every(element => element);
  415. if (!result) {
  416. console.log(results);
  417. }
  418. assert.ok(result);
  419. }
  420. );
  421. it(
  422. 'tolerates missing optional elements (discussion & password disabled)',
  423. function () {
  424. // with discussion = false and password = false in the
  425. // configuration, the template omits these elements entirely
  426. document.documentElement.innerHTML =
  427. '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
  428. 'type="button" class="hidden">New</button></li><li><a ' +
  429. 'id="expiration" href="#" class="hidden">Expiration</a>' +
  430. '</li><li><div id="burnafterreadingoption" class="hidden">' +
  431. 'Burn after reading</div></li><li>' +
  432. '<a id="formatter" href="#" class="hidden">Format</a>' +
  433. '</li><li><button id="sendbutton" type="button" ' +
  434. 'class="hidden">Create</button></li></ul></div></nav>';
  435. PrivateBin.TopNav.init();
  436. assert.doesNotThrow(() => PrivateBin.TopNav.showCreateButtons());
  437. assert.ok(!query('#sendbutton').classList.contains('hidden'));
  438. assert.ok(!query('#burnafterreadingoption').classList.contains('hidden'));
  439. assert.doesNotThrow(() => PrivateBin.TopNav.hideCreateButtons());
  440. assert.ok(query('#sendbutton').classList.contains('hidden'));
  441. assert.ok(query('#burnafterreadingoption').classList.contains('hidden'));
  442. cleanup();
  443. }
  444. );
  445. });
  446. describe('send shortcut', function () {
  447. let sendCount;
  448. // builds the create-document navbar and stubs the actual send, so no
  449. // server interaction happens
  450. function setUpCreateMode() {
  451. cleanup();
  452. sendCount = 0;
  453. document.documentElement.innerHTML =
  454. '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
  455. 'type="button" class="hidden">New</button></li><li><a ' +
  456. 'id="expiration" href="#" class="hidden">Expiration</a>' +
  457. '</li><li><div id="burnafterreadingoption" class="hidden">' +
  458. 'Burn after reading</div></li><li><div id="opendiscussion' +
  459. 'option" class="hidden">Open discussion</div></li><li>' +
  460. '<div id="password" class="hidden">Password</div></li>' +
  461. '<li id="attach" class="hidden">Attach a file</li><li>' +
  462. '<a id="formatter" href="#" class="hidden">Format</a>' +
  463. '</li><li><button id="sendbutton" type="button" ' +
  464. 'class="hidden">Create</button></li></ul></div></nav>';
  465. PrivateBin.PasteEncrypter.sendPaste = function () {
  466. sendCount++;
  467. };
  468. PrivateBin.TopNav.init();
  469. PrivateBin.TopNav.showCreateButtons();
  470. }
  471. // dispatches a keydown on the document, returns whether the default was prevented
  472. function pressKey(options) {
  473. const event = new window.KeyboardEvent('keydown', Object.assign(
  474. {bubbles: true, cancelable: true}, options
  475. ));
  476. document.dispatchEvent(event);
  477. return event.defaultPrevented;
  478. }
  479. afterEach(function () {
  480. cleanup();
  481. });
  482. it('sends the document on Ctrl+S and suppresses the browser save dialog', function () {
  483. setUpCreateMode();
  484. const prevented = pressKey({key: 's', code: 'KeyS', ctrlKey: true});
  485. assert.strictEqual(sendCount, 1);
  486. assert.ok(prevented);
  487. });
  488. it('sends the document on Cmd+S (macOS)', function () {
  489. setUpCreateMode();
  490. const prevented = pressKey({key: 's', code: 'KeyS', metaKey: true});
  491. assert.strictEqual(sendCount, 1);
  492. assert.ok(prevented);
  493. });
  494. it('sends on the physical S key of non-Latin keyboard layouts', function () {
  495. setUpCreateMode();
  496. // on a Russian layout the S key produces "ы"
  497. pressKey({key: 'ы', code: 'KeyS', ctrlKey: true});
  498. assert.strictEqual(sendCount, 1);
  499. });
  500. it('leaves Ctrl+S to the browser when viewing a document', function () {
  501. setUpCreateMode();
  502. PrivateBin.TopNav.hideCreateButtons();
  503. const prevented = pressKey({key: 's', code: 'KeyS', ctrlKey: true});
  504. assert.strictEqual(sendCount, 0);
  505. assert.ok(!prevented);
  506. });
  507. it('ignores S without Ctrl/Cmd, with AltGr (Ctrl+Alt), with Shift, or other keys', function () {
  508. setUpCreateMode();
  509. const prevented = [
  510. pressKey({key: 's', code: 'KeyS'}),
  511. pressKey({key: 's', code: 'KeyS', ctrlKey: true, altKey: true}),
  512. pressKey({key: 'S', code: 'KeyS', ctrlKey: true, shiftKey: true}),
  513. pressKey({key: 'd', code: 'KeyD', ctrlKey: true})
  514. ];
  515. assert.strictEqual(sendCount, 0);
  516. assert.deepStrictEqual(prevented, [false, false, false, false]);
  517. });
  518. });
  519. describe('showNewPasteButton', function () {
  520. before(function () {
  521. cleanup();
  522. });
  523. it(
  524. 'displays the button for creating a document',
  525. function () {
  526. let results = [];
  527. document.documentElement.innerHTML =
  528. '<nav><div id="navbar"><ul><li><button id="newbutton" type=' +
  529. '"button" class="hidden">New</button></li></ul></div></nav>';
  530. PrivateBin.TopNav.init();
  531. results.push(
  532. query('#newbutton').classList.contains('hidden')
  533. );
  534. PrivateBin.TopNav.showNewPasteButton();
  535. results.push(
  536. !query('#newbutton').classList.contains('hidden')
  537. );
  538. cleanup();
  539. const result = results.every(element => element);
  540. if (!result) {
  541. console.log(results);
  542. }
  543. assert.ok(result);
  544. }
  545. );
  546. });
  547. describe('hideCloneButton', function () {
  548. before(function () {
  549. cleanup();
  550. });
  551. it(
  552. 'hides the button for cloning a document',
  553. function () {
  554. let results = [];
  555. document.documentElement.innerHTML =
  556. '<nav><div id="navbar"><ul><li><button id="clonebutton" ' +
  557. 'type="button" class="btn btn-warning navbar-btn">' +
  558. '<span class="glyphicon glyphicon-duplicate" aria-hidden=' +
  559. '"true"></span> Clone</button></li></ul></div></nav>';
  560. PrivateBin.TopNav.init();
  561. results.push(
  562. !query('#clonebutton').classList.contains('hidden')
  563. );
  564. PrivateBin.TopNav.hideCloneButton();
  565. results.push(
  566. query('#clonebutton').classList.contains('hidden')
  567. );
  568. cleanup();
  569. const result = results.every(element => element);
  570. if (!result) {
  571. console.log(results);
  572. }
  573. assert.ok(result);
  574. }
  575. );
  576. });
  577. describe('hideRawButton', function () {
  578. before(function () {
  579. cleanup();
  580. });
  581. it(
  582. 'hides the raw text button',
  583. function () {
  584. let results = [];
  585. document.documentElement.innerHTML =
  586. '<nav><div id="navbar"><ul><li><button ' +
  587. 'id="rawtextbutton" type="button" class="btn ' +
  588. 'btn-warning navbar-btn"><span class="glyphicon ' +
  589. 'glyphicon-text-background" aria-hidden="true"></span> ' +
  590. 'Raw text</button></li></ul></div></nav>';
  591. PrivateBin.TopNav.init();
  592. results.push(
  593. !query('#rawtextbutton').classList.contains('hidden')
  594. );
  595. PrivateBin.TopNav.hideRawButton();
  596. results.push(
  597. query('#rawtextbutton').classList.contains('hidden')
  598. );
  599. cleanup();
  600. const result = results.every(element => element);
  601. if (!result) {
  602. console.log(results);
  603. }
  604. assert.ok(result);
  605. }
  606. );
  607. });
  608. describe('hideFileSelector', function () {
  609. before(function () {
  610. cleanup();
  611. });
  612. it(
  613. 'hides the file attachment selection button',
  614. function () {
  615. let results = [];
  616. document.documentElement.innerHTML =
  617. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  618. 'dropdown"><a href="#" class="dropdown-toggle" data-' +
  619. 'toggle="dropdown" role="button" aria-haspopup="true" ' +
  620. 'aria-expanded="false">Attach a file <span class="caret">' +
  621. '</span></a><ul class="dropdown-menu"><li id="filewrap">' +
  622. '<div><input type="file" id="file" name="file" /></div>' +
  623. '</li><li id="customattachment" class="hidden"></li><li>' +
  624. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  625. '</li></ul></li></ul></div></nav>';
  626. PrivateBin.TopNav.init();
  627. results.push(
  628. !query('#filewrap').classList.contains('hidden')
  629. );
  630. PrivateBin.TopNav.hideFileSelector();
  631. results.push(
  632. query('#filewrap').classList.contains('hidden')
  633. );
  634. cleanup();
  635. const result = results.every(element => element);
  636. if (!result) {
  637. console.log(results);
  638. }
  639. assert.ok(result);
  640. }
  641. );
  642. });
  643. describe('showCustomAttachment', function () {
  644. before(function () {
  645. cleanup();
  646. });
  647. it(
  648. 'display the custom file attachment',
  649. function () {
  650. let results = [];
  651. document.documentElement.innerHTML =
  652. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  653. 'dropdown"><a href="#" class="dropdown-toggle" data-' +
  654. 'toggle="dropdown" role="button" aria-haspopup="true" ' +
  655. 'aria-expanded="false">Attach a file <span class="caret">' +
  656. '</span></a><ul class="dropdown-menu"><li id="filewrap">' +
  657. '<div><input type="file" id="file" name="file" /></div>' +
  658. '</li><li id="customattachment" class="hidden"></li><li>' +
  659. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  660. '</li></ul></li></ul></div></nav>';
  661. PrivateBin.TopNav.init();
  662. results.push(
  663. query('#customattachment').classList.contains('hidden')
  664. );
  665. PrivateBin.TopNav.showCustomAttachment();
  666. results.push(
  667. !query('#customattachment').classList.contains('hidden')
  668. );
  669. cleanup();
  670. const result = results.every(element => element);
  671. if (!result) {
  672. console.log(results);
  673. }
  674. assert.ok(result);
  675. }
  676. );
  677. });
  678. describe('collapseBar', function () {
  679. before(function () {
  680. cleanup();
  681. });
  682. it(
  683. 'collapses the navigation when displayed on a small screen',
  684. function () {
  685. let results = [];
  686. document.documentElement.innerHTML =
  687. '<nav><div class="navbar-header"><button type="button" ' +
  688. 'class="navbar-toggle collapsed" data-toggle="collapse" ' +
  689. 'data-target="#navbar" aria-expanded="false" aria-controls' +
  690. '="navbar">Toggle navigation</button><a class="reloadlink ' +
  691. 'navbar-brand" href=""><img alt="PrivateBin" ' +
  692. 'src="img/icon.svg" width="38" /></a></div><div ' +
  693. 'id="navbar"><ul><li><button id="newbutton" type=' +
  694. '"button" class="hidden">New</button></li></ul></div></nav>';
  695. PrivateBin.TopNav.init();
  696. results.push(
  697. query('.navbar-toggle').classList.contains('collapsed') &&
  698. query('#navbar').getAttribute('aria-expanded') !== 'true'
  699. );
  700. PrivateBin.TopNav.collapseBar();
  701. results.push(
  702. query('.navbar-toggle').classList.contains('collapsed') &&
  703. query('#navbar').getAttribute('aria-expanded') !== 'true'
  704. );
  705. /*
  706. with the upgrade for bootstrap-3.3.7.js to bootstrap-3.4.1.js
  707. the mobile interface detection changed to check if the
  708. ontouchstart event exists, which broke this section of the test
  709. const toggleBtn = document.querySelector('.navbar-toggle');
  710. toggleBtn.dispatchEvent(new MouseEvent('click'));
  711. results.push(
  712. !toggleBtn.classList.contains('collapsed') &&
  713. document.getElementById('navbar').getAttribute('aria-expanded') == 'true'
  714. );
  715. PrivateBin.TopNav.collapseBar();
  716. results.push(
  717. document.querySelector('.navbar-toggle').classList.contains('collapsed') &&
  718. document.getElementById('navbar').getAttribute('aria-expanded') == 'false'
  719. );
  720. */
  721. const result = results.every(element => element);
  722. if (!result) {
  723. console.log(results);
  724. }
  725. assert.ok(result);
  726. }
  727. );
  728. });
  729. describe('resetInput', function () {
  730. before(function () {
  731. cleanup();
  732. });
  733. it(
  734. 'reset inputs to defaults (options off)',
  735. function () {
  736. let results = [];
  737. document.documentElement.innerHTML =
  738. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  739. 'class="hidden"><label><input type="checkbox" ' +
  740. 'id="burnafterreading" name="burnafterreading" /> ' +
  741. 'Burn after reading</label></li><li id="opendiscussionoption" ' +
  742. 'class="hidden"><label><input type="checkbox" ' +
  743. 'id="opendiscussion" name="opendiscussion" /> ' +
  744. 'Open discussion</label></li></ul></div></nav>';
  745. PrivateBin.TopNav.init();
  746. results.push(
  747. !PrivateBin.TopNav.getBurnAfterReading()
  748. );
  749. results.push(
  750. !PrivateBin.TopNav.getOpenDiscussion()
  751. );
  752. query('#burnafterreading').checked = true;
  753. query('#opendiscussion').checked = true;
  754. results.push(
  755. PrivateBin.TopNav.getBurnAfterReading()
  756. );
  757. results.push(
  758. PrivateBin.TopNav.getOpenDiscussion()
  759. );
  760. PrivateBin.TopNav.resetInput();
  761. results.push(
  762. !PrivateBin.TopNav.getBurnAfterReading()
  763. );
  764. results.push(
  765. !PrivateBin.TopNav.getOpenDiscussion()
  766. );
  767. cleanup();
  768. const result = results.every(element => element);
  769. if (!result) {
  770. console.log(results);
  771. }
  772. assert.ok(result);
  773. }
  774. );
  775. it(
  776. 'reset inputs to defaults (burnafterreading on)',
  777. function () {
  778. let results = [];
  779. document.documentElement.innerHTML =
  780. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  781. 'class="hidden"><label><input type="checkbox" ' +
  782. 'id="burnafterreading" name="burnafterreading" checked="checked" /> ' +
  783. 'Burn after reading</label></li><li id="opendiscussionoption" ' +
  784. 'class="hidden"><label><input type="checkbox" ' +
  785. 'id="opendiscussion" name="opendiscussion" checked="checked" /> ' +
  786. 'Open discussion</label></li></ul></div></nav>';
  787. PrivateBin.TopNav.init();
  788. results.push(
  789. PrivateBin.TopNav.getBurnAfterReading()
  790. );
  791. results.push(
  792. !PrivateBin.TopNav.getOpenDiscussion()
  793. );
  794. query('#burnafterreading').checked = false;
  795. query('#burnafterreading').removeAttribute('checked');
  796. results.push(
  797. !PrivateBin.TopNav.getBurnAfterReading()
  798. );
  799. results.push(
  800. !PrivateBin.TopNav.getOpenDiscussion()
  801. );
  802. PrivateBin.TopNav.resetInput();
  803. results.push(
  804. PrivateBin.TopNav.getBurnAfterReading()
  805. );
  806. results.push(
  807. !PrivateBin.TopNav.getOpenDiscussion()
  808. );
  809. cleanup();
  810. const result = results.every(element => element);
  811. if (!result) {
  812. console.log(results);
  813. }
  814. assert.ok(result);
  815. }
  816. );
  817. it(
  818. 'reset inputs to defaults (opendiscussion on)',
  819. function () {
  820. let results = [];
  821. document.documentElement.innerHTML =
  822. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  823. 'class="hidden"><label><input type="checkbox" ' +
  824. 'id="burnafterreading" name="burnafterreading" /> ' +
  825. 'Burn after reading</label></li><li id="opendiscussionoption" ' +
  826. 'class="hidden"><label><input type="checkbox" ' +
  827. 'id="opendiscussion" name="opendiscussion" checked="checked" /> ' +
  828. 'Open discussion</label></li></ul></div></nav>';
  829. PrivateBin.TopNav.init();
  830. results.push(
  831. !PrivateBin.TopNav.getBurnAfterReading()
  832. );
  833. results.push(
  834. PrivateBin.TopNav.getOpenDiscussion()
  835. );
  836. query('#opendiscussion').checked = false;
  837. query('#opendiscussion').removeAttribute('checked');
  838. query('#burnafterreading').checked = true;
  839. query('#burnafterreading').setAttribute('checked', 'checked');
  840. results.push(
  841. PrivateBin.TopNav.getBurnAfterReading()
  842. );
  843. results.push(
  844. !PrivateBin.TopNav.getOpenDiscussion()
  845. );
  846. PrivateBin.TopNav.resetInput();
  847. results.push(
  848. !PrivateBin.TopNav.getBurnAfterReading()
  849. );
  850. results.push(
  851. PrivateBin.TopNav.getOpenDiscussion()
  852. );
  853. cleanup();
  854. const result = results.every(element => element);
  855. if (!result) {
  856. console.log(results);
  857. }
  858. assert.ok(result);
  859. }
  860. );
  861. it(
  862. 'tolerates a missing discussion option (discussion disabled)',
  863. function () {
  864. document.documentElement.innerHTML =
  865. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  866. 'class="buttondisabled"><label><input type="checkbox" ' +
  867. 'id="burnafterreading" name="burnafterreading" /> ' +
  868. 'Burn after reading</label></li></ul></div></nav>';
  869. PrivateBin.TopNav.init();
  870. assert.doesNotThrow(() => PrivateBin.TopNav.resetInput());
  871. assert.ok(!query('#burnafterreadingoption').classList.contains('buttondisabled'));
  872. cleanup();
  873. }
  874. );
  875. });
  876. describe('getExpiration', function () {
  877. before(function () {
  878. cleanup();
  879. });
  880. it(
  881. 'returns the currently selected expiration date',
  882. function () {
  883. document.documentElement.innerHTML =
  884. '<select id="pasteExpiration" name="pasteExpiration">' +
  885. '<option value="1day">1 day</option>' +
  886. '<option value="never">Never</option></select>';
  887. PrivateBin.TopNav.init();
  888. assert.strictEqual(PrivateBin.TopNav.getExpiration(), '1day');
  889. cleanup();
  890. }
  891. );
  892. });
  893. describe('getFileList', function () {
  894. before(function () {
  895. cleanup();
  896. });
  897. const File = window.File,
  898. FileList = window.FileList,
  899. path = require('path'), // eslint-disable-line global-require
  900. mime = require('mime-types'); // eslint-disable-line global-require
  901. // mocking file input as per https://github.com/jsdom/jsdom/issues/1272
  902. function createFile(file_path) {
  903. const lastModified = fs.statSync(file_path).mtimeMs;
  904. return new File(
  905. [new fs.readFileSync(file_path)],
  906. path.basename(file_path),
  907. {
  908. lastModified,
  909. type: mime.lookup(file_path) || ''
  910. }
  911. );
  912. }
  913. function addFileList(input, file_paths) {
  914. if (typeof file_paths === 'string') {
  915. file_paths = [file_paths];
  916. } else if (!Array.isArray(file_paths)) {
  917. throw new Error('file_paths needs to be a file path string or an Array of file path strings');
  918. }
  919. const file_list = file_paths.map(fp => createFile(fp));
  920. Object.setPrototypeOf(file_list, Object.create(FileList.prototype));
  921. Object.defineProperty(input, 'files', {
  922. value: file_list,
  923. writeable: false
  924. });
  925. return input;
  926. }
  927. it(
  928. 'returns the selected files',
  929. function () {
  930. let results = [];
  931. document.documentElement.innerHTML =
  932. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  933. 'dropdown"><a href="#" class="dropdown-toggle" data-' +
  934. 'toggle="dropdown" role="button" aria-haspopup="true" ' +
  935. 'aria-expanded="false">Attach a file <span class="caret">' +
  936. '</span></a><ul class="dropdown-menu"><li id="filewrap">' +
  937. '<div><input type="file" id="file" name="file" /></div>' +
  938. '</li><li id="customattachment" class="hidden"></li><li>' +
  939. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  940. '</li></ul></li></ul></div></nav>';
  941. PrivateBin.TopNav.init();
  942. results.push(
  943. PrivateBin.TopNav.getFileList() === null
  944. );
  945. addFileList(query('#file'), [
  946. '../img/logo.svg',
  947. '../img/busy.gif'
  948. ]);
  949. const files = PrivateBin.TopNav.getFileList();
  950. results.push(
  951. files[0].name === 'logo.svg' &&
  952. files[1].name === 'busy.gif'
  953. );
  954. cleanup();
  955. const result = results.every(element => element);
  956. if (!result) {
  957. console.log(results);
  958. }
  959. assert.ok(result);
  960. }
  961. );
  962. });
  963. describe('getBurnAfterReading', function () {
  964. before(function () {
  965. cleanup();
  966. });
  967. it(
  968. 'returns if the burn-after-reading checkbox was ticked',
  969. function () {
  970. let results = [];
  971. document.documentElement.innerHTML =
  972. '<nav><div id="navbar"><ul><li id="burnafterreadingoption" ' +
  973. 'class="hidden"><label><input type="checkbox" ' +
  974. 'id="burnafterreading" name="burnafterreading" /> ' +
  975. 'Burn after reading</label></li></ul></div></nav>';
  976. PrivateBin.TopNav.init();
  977. results.push(
  978. !PrivateBin.TopNav.getBurnAfterReading()
  979. );
  980. query('#burnafterreading').checked = true;
  981. results.push(
  982. PrivateBin.TopNav.getBurnAfterReading()
  983. );
  984. query('#burnafterreading').checked = false;
  985. query('#burnafterreading').removeAttribute('checked');
  986. results.push(
  987. !PrivateBin.TopNav.getBurnAfterReading()
  988. );
  989. cleanup();
  990. const result = results.every(element => element);
  991. if (!result) {
  992. console.log(results);
  993. }
  994. assert.ok(result);
  995. }
  996. );
  997. });
  998. describe('getOpenDiscussion', function () {
  999. before(function () {
  1000. cleanup();
  1001. });
  1002. it(
  1003. 'returns if the open-discussion checkbox was ticked',
  1004. function () {
  1005. let results = [];
  1006. document.documentElement.innerHTML =
  1007. '<nav><div id="navbar"><ul><li id="opendiscussionoption" ' +
  1008. 'class="hidden"><label><input type="checkbox" ' +
  1009. 'id="opendiscussion" name="opendiscussion" /> ' +
  1010. 'Open discussion</label></li></ul></div></nav>';
  1011. PrivateBin.TopNav.init();
  1012. results.push(
  1013. !PrivateBin.TopNav.getOpenDiscussion()
  1014. );
  1015. query('#opendiscussion').checked = true;
  1016. results.push(
  1017. PrivateBin.TopNav.getOpenDiscussion()
  1018. );
  1019. query('#opendiscussion').checked = false;
  1020. query('#opendiscussion').removeAttribute('checked');
  1021. results.push(
  1022. !PrivateBin.TopNav.getOpenDiscussion()
  1023. );
  1024. cleanup();
  1025. const result = results.every(element => element);
  1026. if (!result) {
  1027. console.log(results);
  1028. }
  1029. assert.ok(result);
  1030. }
  1031. );
  1032. });
  1033. describe('getPassword', function () {
  1034. before(function () {
  1035. globalThis.cleanup();
  1036. });
  1037. it('returns the contents of the password input', () => {
  1038. fc.assert(fc.property(
  1039. fc.string(),
  1040. function (password) {
  1041. password = password.replace(/\r+|\n+/g, '');
  1042. let results = [];
  1043. document.documentElement.innerHTML =
  1044. '<nav><div id="navbar"><ul><li><div id="password" ' +
  1045. 'class="navbar-form hidden"><input type="password" ' +
  1046. 'id="passwordinput" placeholder="Password (recommended)" ' +
  1047. 'class="form-control" size="23" /></div></li></ul></div></nav>';
  1048. PrivateBin.TopNav.init();
  1049. results.push(
  1050. PrivateBin.TopNav.getPassword() === ''
  1051. );
  1052. query('#passwordinput').value = password;
  1053. results.push(
  1054. PrivateBin.TopNav.getPassword() === password
  1055. );
  1056. query('#passwordinput').value = '';
  1057. results.push(
  1058. PrivateBin.TopNav.getPassword() === ''
  1059. );
  1060. globalThis.cleanup();
  1061. const result = results.every(element => element);
  1062. if (!result) {
  1063. console.log(results);
  1064. }
  1065. return result;
  1066. }
  1067. ));
  1068. });
  1069. });
  1070. describe('getCustomAttachment', function () {
  1071. before(function () {
  1072. cleanup();
  1073. });
  1074. it(
  1075. 'returns the custom attachment element',
  1076. function () {
  1077. let results = [];
  1078. document.documentElement.innerHTML =
  1079. '<nav><div id="navbar"><ul><li id="attach" class="hidden ' +
  1080. 'dropdown"><a href="#" class="dropdown-toggle" data-' +
  1081. 'toggle="dropdown" role="button" aria-haspopup="true" ' +
  1082. 'aria-expanded="false">Attach a file <span class="caret">' +
  1083. '</span></a><ul class="dropdown-menu"><li id="filewrap">' +
  1084. '<div><input type="file" id="file" name="file" /></div>' +
  1085. '</li><li id="customattachment" class="hidden"></li><li>' +
  1086. '<a id="fileremovebutton" href="#">Remove attachment</a>' +
  1087. '</li></ul></li></ul></div></nav>';
  1088. PrivateBin.TopNav.init();
  1089. results.push(
  1090. !PrivateBin.TopNav.getCustomAttachment().classList.contains('test')
  1091. );
  1092. query('#customattachment').classList.add('test');
  1093. results.push(
  1094. PrivateBin.TopNav.getCustomAttachment().classList.contains('test')
  1095. );
  1096. cleanup();
  1097. const result = results.every(element => element);
  1098. if (!result) {
  1099. console.log(results);
  1100. }
  1101. assert.ok(result);
  1102. }
  1103. );
  1104. });
  1105. describe('hideAllButtons', function () {
  1106. before(function () {
  1107. cleanup();
  1108. });
  1109. it(
  1110. 'hides all buttons correctly',
  1111. function () {
  1112. // Insert any setup code needed for the hideAllButtons function
  1113. // Example: Initialize the DOM elements required for testing
  1114. document.body.innerHTML =
  1115. '<nav class="navbar navbar-inverse navbar-static-top">' +
  1116. '<div id="navbar" class="navbar-collapse collapse"><ul ' +
  1117. 'class="nav navbar-nav"><li><button id="newbutton" ' +
  1118. 'type="button" class="hidden btn btn-warning navbar-btn">' +
  1119. '<span class="glyphicon glyphicon-file" aria-hidden="true">' +
  1120. '</span> New</button><button id="clonebutton" type="button"' +
  1121. ' class="hidden btn btn-warning navbar-btn">' +
  1122. '<span class="glyphicon glyphicon-duplicate" ' +
  1123. 'aria-hidden="true"></span> Clone</button><button ' +
  1124. 'id="rawtextbutton" type="button" class="hidden btn ' +
  1125. 'btn-warning navbar-btn"><span class="glyphicon ' +
  1126. 'glyphicon-text-background" aria-hidden="true"></span> ' +
  1127. 'Raw text</button><button id="qrcodelink" type="button" ' +
  1128. 'data-toggle="modal" data-target="#qrcodemodal" ' +
  1129. 'class="hidden btn btn-warning navbar-btn"><span ' +
  1130. 'class="glyphicon glyphicon-qrcode" aria-hidden="true">' +
  1131. '</span> QR code</button></li></ul></div></nav>';
  1132. PrivateBin.TopNav.init();
  1133. PrivateBin.TopNav.hideAllButtons();
  1134. assert.ok(query('#newbutton').classList.contains('hidden'));
  1135. assert.ok(query('#clonebutton').classList.contains('hidden'));
  1136. assert.ok(query('#rawtextbutton').classList.contains('hidden'));
  1137. assert.ok(query('#qrcodelink').classList.contains('hidden'));
  1138. cleanup();
  1139. }
  1140. );
  1141. });
  1142. describe('showRetryButton & hideRetryButton', function () {
  1143. afterEach(function () {
  1144. delete global.bootstrap;
  1145. cleanup();
  1146. });
  1147. it(
  1148. 'shows and hides the Bootstrap 5 retry button with its list item and runs the callback once',
  1149. function () {
  1150. cleanup();
  1151. global.bootstrap = {};
  1152. document.documentElement.innerHTML =
  1153. '<nav><div id="navbar"><ul class="navbar-nav">' +
  1154. '<li class="nav-item hidden"><button id="retrybutton" ' +
  1155. 'type="button" class="hidden">Retry</button></li>' +
  1156. '<li><button id="newbutton" type="button">New</button>' +
  1157. '</li></ul></div></nav>';
  1158. PrivateBin.TopNav.init();
  1159. let calls = 0;
  1160. PrivateBin.TopNav.setRetryCallback(function () {
  1161. calls++;
  1162. });
  1163. const retryItem = query('#retrybutton').parentElement;
  1164. assert.ok(query('#retrybutton').classList.contains('hidden'));
  1165. assert.ok(retryItem.classList.contains('hidden'));
  1166. PrivateBin.TopNav.showRetryButton();
  1167. assert.ok(!query('#retrybutton').classList.contains('hidden'));
  1168. assert.ok(!retryItem.classList.contains('hidden'));
  1169. query('#retrybutton').click();
  1170. assert.strictEqual(calls, 1);
  1171. PrivateBin.TopNav.hideRetryButton();
  1172. assert.ok(query('#retrybutton').classList.contains('hidden'));
  1173. assert.ok(retryItem.classList.contains('hidden'));
  1174. PrivateBin.TopNav.showRetryButton();
  1175. PrivateBin.TopNav.hideRetryButton();
  1176. assert.ok(query('#retrybutton').classList.contains('hidden'));
  1177. assert.ok(retryItem.classList.contains('hidden'));
  1178. assert.ok(!query('#newbutton').classList.contains('hidden'));
  1179. assert.strictEqual(calls, 1);
  1180. }
  1181. );
  1182. it(
  1183. 'toggles only the retry button on legacy Bootstrap 3 and page templates',
  1184. function () {
  1185. cleanup();
  1186. delete global.bootstrap;
  1187. document.documentElement.innerHTML =
  1188. '<nav><div id="navbar"><ul class="nav navbar-nav"><li>' +
  1189. '<button id="retrybutton" type="button" class="hidden">' +
  1190. 'Retry</button><button id="newbutton" type="button">' +
  1191. 'New</button></li></ul></div></nav>';
  1192. PrivateBin.TopNav.init();
  1193. let calls = 0;
  1194. PrivateBin.TopNav.setRetryCallback(function () {
  1195. calls++;
  1196. });
  1197. const retryItem = query('#retrybutton').parentElement;
  1198. assert.ok(query('#retrybutton').classList.contains('hidden'));
  1199. assert.ok(!retryItem.classList.contains('hidden'));
  1200. PrivateBin.TopNav.showRetryButton();
  1201. assert.ok(!query('#retrybutton').classList.contains('hidden'));
  1202. assert.ok(!retryItem.classList.contains('hidden'));
  1203. assert.ok(!query('#newbutton').classList.contains('hidden'));
  1204. query('#retrybutton').click();
  1205. assert.strictEqual(calls, 1);
  1206. PrivateBin.TopNav.hideRetryButton();
  1207. assert.ok(query('#retrybutton').classList.contains('hidden'));
  1208. assert.ok(!retryItem.classList.contains('hidden'));
  1209. assert.ok(!query('#newbutton').classList.contains('hidden'));
  1210. PrivateBin.TopNav.showRetryButton();
  1211. PrivateBin.TopNav.hideRetryButton();
  1212. assert.ok(query('#retrybutton').classList.contains('hidden'));
  1213. assert.strictEqual(calls, 1);
  1214. }
  1215. );
  1216. });
  1217. describe('rawText', function () {
  1218. before(function () {
  1219. cleanup();
  1220. });
  1221. // TODO triggers error messages in jsDOM since version 12, but passes
  1222. it(
  1223. 'displays raw text view correctly',
  1224. function () {
  1225. const clean = globalThis.cleanup('', {url: 'https://privatebin.net/?0123456789abcdef#1'});
  1226. document.documentElement.innerHTML = `
  1227. <li id="loadingindicator" class="hidden"></li>
  1228. <button id="rawtextbutton"></button>`;
  1229. const sample = 'example';
  1230. PrivateBin.Alert.init(); // required because of locading indiator being used
  1231. PrivateBin.TopNav.init();
  1232. PrivateBin.PasteViewer.setText(sample);
  1233. PrivateBin.Helper.reset();
  1234. query('#rawtextbutton').click();
  1235. assert.strictEqual(document.querySelector('pre').textContent, sample);
  1236. clean();
  1237. }
  1238. );
  1239. });
  1240. });